fix: allow queueing behind active blockers
This commit is contained in:
parent
b4cb9fbc41
commit
356d06ef58
0
.yoi/tickets/00001KVHQDS6B/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHQDS6B/artifacts/.gitkeep
Normal file
21
.yoi/tickets/00001KVHQDS6B/artifacts/relations.json
Normal file
21
.yoi/tickets/00001KVHQDS6B/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHQDS6B",
|
||||
"kind": "related",
|
||||
"target": "00001KVHKWNQA",
|
||||
"note": "Observed prerequisite Ticket already queued/in progress should allow dependent queueing.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:19:32Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHQDS6B",
|
||||
"kind": "related",
|
||||
"target": "00001KVHKWNQS",
|
||||
"note": "Dependent Ticket hit backend conflict despite Panel queue UI allowing it.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:19:32Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
55
.yoi/tickets/00001KVHQDS6B/item.md
Normal file
55
.yoi/tickets/00001KVHQDS6B/item.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
title: 'Panel Queue action should allow ready Tickets whose blockers are already queued or in progress'
|
||||
state: 'done'
|
||||
created_at: '2026-06-20T05:18:00Z'
|
||||
updated_at: '2026-06-20T05:19:32Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['ticket', 'panel', 'queue', 'dependency', 'blocker', 'orchestrator']
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Panel ViewModel already treats a ready Ticket as queueable when all blocking relations point at Tickets that are already `queued` or `inprogress`. That matches the intended workflow: once prerequisite work is queued/accepted by Orchestrator, dependent ready Tickets should also be queueable so the human does not need to return after every prerequisite completes.
|
||||
|
||||
However, pressing Enter in Panel still calls the backend `queue_ready` gate, and that backend rejected any unresolved relation blocker regardless of blocker state. This caused a `ticket conflict` even though the UI correctly showed Queue as available.
|
||||
|
||||
Concrete example:
|
||||
|
||||
- `00001KVHKWNQS` depends on `00001KVHKWNQA`.
|
||||
- `00001KVHKWNQA` is already `inprogress`.
|
||||
- Panel shows `00001KVHKWNQS` as queueable.
|
||||
- Enter/Queue fails with backend ticket conflict.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Backend `queue_ready` must match the Panel gating rule.
|
||||
- A ready Ticket may transition `ready -> queued` when every relation blocker is already `queued` or `inprogress`.
|
||||
- A ready Ticket must still be blocked when any relation blocker is `planning` or `ready`.
|
||||
- This change applies to queueing only.
|
||||
- `queued -> inprogress` acceptance remains blocked while unresolved dependency/blocker relations exist; Orchestrator must preserve execution order after queueing.
|
||||
- Existing Panel display behavior should remain aligned with backend behavior.
|
||||
|
||||
## Implementation summary
|
||||
|
||||
- Added backend helper `relation_blocker_allows_queue`.
|
||||
- Updated `LocalTicketBackend::queue_ready` to filter relation blockers by that helper before rejecting.
|
||||
- Kept `queued -> inprogress` relation blocker validation unchanged.
|
||||
- Added backend test coverage for ready Tickets with queued/inprogress blockers.
|
||||
- Re-ran existing Panel ViewModel test that already asserted ready+queued dependency appears queueable.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `ready` Ticket with `depends_on` target in `queued` state can be queued.
|
||||
- `ready` Ticket with incoming `blocks` blocker in `inprogress` state can be queued.
|
||||
- `ready` Ticket with dependency/blocker still in `planning` remains rejected.
|
||||
- Panel and backend queue behavior are consistent.
|
||||
- Orchestrator acceptance ordering remains guarded by existing `queued -> inprogress` relation check.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p ticket queue_gate --lib`
|
||||
- `cargo test -p tui workspace_panel_allows_ready_ticket_when_relation_prerequisite_is_queued --lib`
|
||||
- `cargo check -p ticket -p tui`
|
||||
- `cargo fmt`
|
||||
- `git diff --check`
|
||||
7
.yoi/tickets/00001KVHQDS6B/thread.md
Normal file
7
.yoi/tickets/00001KVHQDS6B/thread.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:18:00Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
|
@ -1650,11 +1650,15 @@ impl TicketBackend for LocalTicketBackend {
|
|||
let item = dir.join("item.md");
|
||||
let meta = ticket_meta_for_dir(&dir, read_item_file(&item)?.frontmatter)?;
|
||||
let blockers = self.relation_blockers_for_meta(&meta)?;
|
||||
if !blockers.is_empty() {
|
||||
let active_blockers = blockers
|
||||
.into_iter()
|
||||
.filter(|blocker| !relation_blocker_allows_queue(blocker))
|
||||
.collect::<Vec<_>>();
|
||||
if !active_blockers.is_empty() {
|
||||
return Err(TicketError::Conflict(format!(
|
||||
"ticket {} has unresolved blocking relation(s): {}",
|
||||
meta.id,
|
||||
format_relation_blockers(&blockers)
|
||||
format_relation_blockers(&active_blockers)
|
||||
)));
|
||||
}
|
||||
let at = now_utc();
|
||||
|
|
@ -2550,6 +2554,13 @@ fn relation_view_from_records(
|
|||
view
|
||||
}
|
||||
|
||||
fn relation_blocker_allows_queue(blocker: &TicketRelationBlocker) -> bool {
|
||||
matches!(
|
||||
blocker.blocking_state,
|
||||
TicketWorkflowState::Queued | TicketWorkflowState::InProgress
|
||||
)
|
||||
}
|
||||
|
||||
fn format_relation_blockers(blockers: &[TicketRelationBlocker]) -> String {
|
||||
blockers
|
||||
.iter()
|
||||
|
|
@ -4408,6 +4419,65 @@ state: planning
|
|||
assert_eq!(backend.doctor().unwrap().error_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_gate_allows_ready_ticket_when_blocking_relation_is_already_queued_or_inprogress() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let backend = backend(&tmp);
|
||||
let mut waiter_input = NewTicket::new("Ready After Queued Dependency");
|
||||
waiter_input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
let waiter = backend.create(waiter_input).unwrap();
|
||||
let mut dependency_input = NewTicket::new("Queued Dependency");
|
||||
dependency_input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
let dependency = backend.create(dependency_input).unwrap();
|
||||
backend
|
||||
.add_ticket_relation(
|
||||
TicketIdOrSlug::Id(waiter.id.clone()),
|
||||
NewTicketRelation {
|
||||
kind: TicketRelationKind::DependsOn,
|
||||
target: dependency.id.clone(),
|
||||
note: None,
|
||||
author: Some("test".to_string()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.queue_ready(TicketIdOrSlug::Id(waiter.id.clone()), "test")
|
||||
.unwrap();
|
||||
let queued = backend.show(TicketIdOrSlug::Id(waiter.id.clone())).unwrap();
|
||||
assert_eq!(queued.meta.workflow_state, TicketWorkflowState::Queued);
|
||||
assert_eq!(queued.meta.queued_by.as_deref(), Some("test"));
|
||||
|
||||
let mut incoming_input = NewTicket::new("Ready After Inprogress Blocker");
|
||||
incoming_input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
let incoming = backend.create(incoming_input).unwrap();
|
||||
let mut blocker_input = NewTicket::new("Inprogress Blocker");
|
||||
blocker_input.workflow_state = Some(TicketWorkflowState::InProgress);
|
||||
let blocker = backend.create(blocker_input).unwrap();
|
||||
backend
|
||||
.add_ticket_relation(
|
||||
TicketIdOrSlug::Id(blocker.id.clone()),
|
||||
NewTicketRelation {
|
||||
kind: TicketRelationKind::Blocks,
|
||||
target: incoming.id.clone(),
|
||||
note: None,
|
||||
author: Some("test".to_string()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.queue_ready(TicketIdOrSlug::Id(incoming.id.clone()), "test")
|
||||
.unwrap();
|
||||
let queued_incoming = backend
|
||||
.show(TicketIdOrSlug::Id(incoming.id.clone()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
queued_incoming.meta.workflow_state,
|
||||
TicketWorkflowState::Queued
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_gate_rejects_unresolved_dependency_and_incoming_blocker() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user