merge: integrate orchestrator panel queue sync
This commit is contained in:
commit
edb736f4ce
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260612-091119-1","ticket_id":"00001KTVPS6K3","kind":"waiting_capacity_note","note":"Workspace Panel Queue notification was received, but this Orchestrator cannot safely accept implementation yet. The Orchestrator Ticket backend still reads the Ticket as `ready`, while the root workspace has unsynced/uncommitted queued Ticket changes for `00001KTVPS6K3`; root workspace is dirty (`.yoi/tickets/00001KTVPS6K3/*` plus `crates/tui/src/multi_pod.rs`). Current active work `00001KTWPE3KQ` is also in review and is specifically fixing the Panel Queue durable handoff/sync path. Re-route this Ticket after the Queue handoff is safely synced/committed and active review/merge state is clear.","author":"orchestrator","at":"2026-06-12T09:11:19Z"}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
title: 'Ticket role launch inputを短縮し、role behaviorをInstruction/Workflowへ分離する'
|
||||
state: 'ready'
|
||||
created_at: '2026-06-11T16:03:28Z'
|
||||
updated_at: '2026-06-11T16:03:33Z'
|
||||
updated_at: '2026-06-12T09:11:30Z'
|
||||
assignee: null
|
||||
risk_flags: ['prompt-context', 'workflow-boundary', 'role-launch']
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,3 +21,30 @@ Ticket role launch の初回 user message を短縮し、情報所管を Instruc
|
|||
要件・受け入れ条件・境界判断が揃ったため、Orchestrator routing 可能。実装はユーザーが panel で queue した後に開始する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: orchestrator at: 2026-06-12T09:11:30Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_by_dependency_or_missing_authority
|
||||
|
||||
Reason:
|
||||
- Workspace Panel Queue notification was received, but this Orchestrator backend still reads the Ticket as `ready`, not `queued`.
|
||||
- The root workspace has unsynced/uncommitted queue-related changes for this Ticket and is dirty, including `.yoi/tickets/00001KTVPS6K3/*` and `crates/tui/src/multi_pod.rs`.
|
||||
- Active in-progress work `00001KTWPE3KQ` is currently fixing the Panel Queue durable handoff/sync path and is in reviewer handoff; accepting this new Ticket before that path is reviewed/merged would require manual sync/queue recovery and could mix queue-side effects with unrelated dirty root changes.
|
||||
|
||||
Evidence checked:
|
||||
- TicketShow `00001KTVPS6K3`: Orchestrator backend state is `ready`.
|
||||
- TicketRelationQuery: no relation blockers.
|
||||
- TicketOrchestrationPlanQuery: no prior plan records before this routing note.
|
||||
- Root/orchestrator git state: Orchestrator branch has local routing record changes; root workspace is dirty with this Ticket's `.yoi` files and `crates/tui/src/multi_pod.rs`.
|
||||
- Visible Pods: active reviewer `yoi-reviewer-panel-queue-sync` for `00001KTWPE3KQ`.
|
||||
|
||||
Next action:
|
||||
- Leave this Ticket unaccepted for implementation in this Orchestrator pass.
|
||||
- Re-route after the Panel Queue durable handoff work is resolved and the root/orchestration Ticket state is synchronized cleanly, or after a human explicitly instructs manual recovery for the queued root-side changes.
|
||||
|
||||
Escalate if:
|
||||
- The queued root-side changes should be manually committed/synced despite the current dirty workspace and active Queue-handoff fix.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260612-084329-1","ticket_id":"00001KTWPE3KQ","kind":"accepted_plan","note":"Role Pods は今回起動しない。明示 follow-up まで queued のまま保持する。","accepted_plan":{"summary":"Routing では implementation_ready と判断した。ただし今回の launch instruction は role Pod spawn を explicit follow-up まで待つ指定のため、現時点では queued のまま保持し、queued -> inprogress、worktree 作成、coder/reviewer spawn、merge/close は行わない。実装開始時は side effect 前に TicketShow / relation / orchestration plan / git/worktree state を再確認し、問題なければ queued -> inprogress を記録してから進める。実装対象は Panel Queue action を root/dev 側 Ticket mutation + Queue commit + orchestration worktree ff-only sync + sync 確認後 Orchestrator notify/kick という durable handoff にすること。","branch":"ticket/panel-queue-orchestrator-sync","worktree":"/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync","role_plan":"次の明示 follow-up 後に Orchestrator が worktree-workflow で実装 worktree を作り、coder はその worktree に narrow write scope、reviewer は read-only scopeで sibling として起動する。Queue commit の対象差分限定、root/orchestration worktree identity checks、ff-only sync、dirty/divergent workspace block、workspace_root と cwd の分離維持を reviewer focus とする。"},"author":"orchestrator","at":"2026-06-12T08:43:29Z"}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Panel Queue時にdevとOrchestrator worktreeを同期する'
|
||||
state: 'queued'
|
||||
state: 'inprogress'
|
||||
created_at: '2026-06-12T01:16:39Z'
|
||||
updated_at: '2026-06-12T02:39:25Z'
|
||||
updated_at: '2026-06-12T09:12:25Z'
|
||||
assignee: null
|
||||
queued_by: 'yoi ticket'
|
||||
queued_at: '2026-06-12T02:39:25Z'
|
||||
|
|
|
|||
|
|
@ -30,4 +30,230 @@ Marked ready by `yoi ticket state`.
|
|||
Ticket を `yoi ticket` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: orchestrator at: 2026-06-12T08:43:58Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Ticket body は Panel Queue action の目的、binding invariants、失敗時の block 条件、非目標、受け入れ条件を observable に記録している。
|
||||
- `ready -> queued` は既に記録済みで、relation blocker はない。
|
||||
- 設計境界は「Ticket tools は cwd 基準」「Panel は root/dev 側の Queue commit を authority とし、Orchestrator kick 前に orchestration worktree を ff-only sync する」「自動 conflict resolution / stash / rebase はしない」として十分に固定されている。
|
||||
- 残る不確実性は既存 Panel/Ticket/git helper 実装への bounded implementation investigation と local tactic selection に閉じている。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread / artifacts: `00001KTWPE3KQ` の本文、state/event、artifact なしを確認。
|
||||
- TicketRelationQuery: outgoing/incoming relation なし、blocker なし。
|
||||
- TicketOrchestrationPlanQuery: 既存 record なし。今回 `orch-plan-20260612-084329-1` として accepted plan を記録。
|
||||
- Workspace state: Orchestrator worktree は `## ticket/orchestrator-progress-companion-notify`、root workspace は `## develop`、どちらも dirty 出力なし。
|
||||
- Code map: `crates/tui/src/workspace_panel.rs` と `crates/tui/src/multi_pod.rs` 周辺に Panel Queue / workspace panel / orchestrator launch の実装面があることを確認。
|
||||
- Visible Pods: 現在 visible Pod はこの Orchestrator のみで、coder/reviewer は未起動。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Panel Queue action を、root/dev 側の `ready -> queued` commit と orchestration worktree への ff-only sync を完了してから Orchestrator notify/kick する durable handoff にする。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- root workspace の canonical top-level、orchestration worktree の canonical top-level、共通 git dir、期待 branch、dirty 状態、対象 Ticket の `ready` 状態、orchestration_head が root_head の ancestor であることを mutation 前に検証する。
|
||||
- Queue commit は root/dev 側で対象 Ticket record だけを stage/commit する。
|
||||
- orchestration worktree への自動同期は `git -C <orchestration_worktree> merge --ff-only <queue_commit_sha>` のみに限定する。
|
||||
- merge commit、rebase、stash、patch apply、conflict resolution、dirty cleanup は Panel Queue action では行わない。
|
||||
- Orchestrator notify/restore/kick は orchestration worktree HEAD が Queue commit を含み、orchestration worktree 側 Ticket backend で対象 Ticket が `queued` として読めることを確認した後だけ行う。
|
||||
- Ticket tools が cwd 基準で動く設計と、workspace_root / cwd / orchestration worktree / merge target の分離を崩さない。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Queue 成功時、Panel は queued Ticket id、dev 側 Queue commit sha、orchestration worktree sync 結果、Orchestrator notify/kick の有無を表示する。
|
||||
- root dirty、orchestration dirty、branch divergence、non-ff sync required、Ticket state mismatch、worktree identity mismatch では Queue を block し、失敗した check 名と対象 path / branch / Ticket id を表示する。
|
||||
- `nix build .#yoi` が通る。
|
||||
|
||||
Implementation latitude:
|
||||
- 既存 Panel action / Git helper / Ticket backend 呼び出しのどこへ checks と sync 処理を分割するかは coder が調査して選んでよい。
|
||||
- 表示文言や内部 helper 名は、失敗条件が具体的に伝わり、既存 UX と整合する範囲で調整してよい。
|
||||
- Focused tests / unit coverage の追加位置は既存の TUI/client/test 構造に合わせてよい。
|
||||
|
||||
Escalate if:
|
||||
- Queue action が root/dev 側 Ticket commit 以外の変更を commit/stage する必要が出る。
|
||||
- ff-only 以外の sync、stash/rebase/conflict resolution、dirty cleanup を自動化しないと成立しない。
|
||||
- Ticket tools の cwd 基準設計、Pod workspace_root/cwd 分離、dedicated Orchestrator worktree の authority boundary を変える必要が出る。
|
||||
- Panel が Orchestrator routing/acceptance を代行する必要が出る。
|
||||
|
||||
Validation:
|
||||
- 変更に対応する focused test / existing relevant cargo test。
|
||||
- `cargo fmt --check`。
|
||||
- `git diff --check`。
|
||||
- `target/debug/yoi ticket doctor`。
|
||||
- runtime resource / packaging / prompt へ触れる場合、または最終確認として `nix build .#yoi`。
|
||||
|
||||
Current code map:
|
||||
- 主な調査対象: `crates/tui/src/workspace_panel.rs`, `crates/tui/src/multi_pod.rs`。
|
||||
- Ticket backend / state mutation / CLI helper を変更する場合は `crates/ticket` と `crates/yoi` 側の既存 typed Ticket path に合わせる。
|
||||
- Orchestrator/main `.yoi` project records、memory/local/runtime/secret-like `.yoi` paths は implementation worktree 側で勝手に生成・編集しない。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- root workspace と orchestration worktree を取り違えないこと。
|
||||
- dirty/divergent 状態で mutation/commit/notify しないこと。
|
||||
- Queue commit の差分が対象 Ticket record 以外へ広がらないこと。
|
||||
- ff-only 限定を破らないこと。
|
||||
- notify/kick が sync 完了前に走らないこと。
|
||||
- cwd 基準 Ticket tools と workspace_root 分離を壊さないこと。
|
||||
|
||||
Next action:
|
||||
- 今回の launch instruction は role Pod spawn を explicit follow-up まで待つ指定なので、ここでは `queued -> inprogress`、worktree 作成、coder/reviewer spawn、merge/close は行わない。
|
||||
- 明示 follow-up があれば、side effect 前に TicketShow / relation / orchestration plan / git/worktree state を再確認し、問題がなければ `queued -> inprogress` を記録してから `multi-agent-workflow` に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: orchestrator at: 2026-06-12T08:45:20Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Routing accepted for implementation. Ticket body/thread, relation blockers, orchestration plan, visible Pods, and root/orchestration git state were rechecked. No unresolved dependency/blocker or missing planning decision was found. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded; worktree and sibling coder/reviewer routing will use the accepted plan `orch-plan-20260612-084329-1`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: orchestrator at: 2026-06-12T08:45:59Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Implementation worktree created for multi-agent handoff.
|
||||
|
||||
- Ticket: `00001KTWPE3KQ`
|
||||
- Branch: `ticket/panel-queue-orchestrator-sync`
|
||||
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||
- Base: Orchestrator workspace HEAD `de0f533b` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: orchestrator at: 2026-06-12T08:47:16Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Coder delegated.
|
||||
|
||||
- Coder Pod: `yoi-coder-panel-queue-sync`
|
||||
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||
- Branch: `ticket/panel-queue-orchestrator-sync`
|
||||
- Scope: read `/home/hare/Projects/yoi`, write `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||
- Task: implement Panel Queue durable handoff with root/dev Queue commit, orchestration worktree ff-only sync, post-sync Ticket verification, and notify/kick ordering. Coder was instructed not to edit Orchestrator/main `.yoi` records or generated memory/runtime/secret-like paths.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: orchestrator at: 2026-06-12T09:05:32Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder completed initial implementation.
|
||||
|
||||
- Coder Pod: `yoi-coder-panel-queue-sync`
|
||||
- Implementation branch: `ticket/panel-queue-orchestrator-sync`
|
||||
- Implementation commit: `04a3c6e0` (`tui: make panel queue handoff durable`)
|
||||
- Worktree status checked clean.
|
||||
|
||||
Orchestrator validation performed after coder handoff:
|
||||
- `git diff --check HEAD^..HEAD` — pass
|
||||
- `cargo test -p tui ticket_queue_action -- --nocapture` — pass
|
||||
- `cargo fmt --check` — pass
|
||||
- `cargo check -p tui --all-targets` — pass
|
||||
- `nix build .#yoi` — pass
|
||||
- `./result/bin/yoi ticket doctor` — `doctor: ok`
|
||||
|
||||
Next: delegate independent reviewer against the recorded Ticket intent, accepted plan, implementation commit, and validation evidence.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: orchestrator at: 2026-06-12T09:06:03Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Reviewer delegated.
|
||||
|
||||
- Reviewer Pod: `yoi-reviewer-panel-queue-sync`
|
||||
- Review target: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`, branch `ticket/panel-queue-orchestrator-sync`, commit `04a3c6e0`
|
||||
- Scope: read-only `/home/hare/Projects/yoi`
|
||||
- Reviewer focus: durable Queue handoff ordering, root/orchestration worktree identity and cleanliness checks, ff-only-only sync, pathscope of Queue commit, no auto conflict/stash/rebase, notify only after sync + Ticket verification, and whether `--no-verify` is acceptable.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: orchestrator at: 2026-06-12T09:12:16Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Merge-ready dossier (human/parent merge authority required before merge/close/cleanup)
|
||||
|
||||
Ticket:
|
||||
- `00001KTWPE3KQ` — Panel Queue時にdevとOrchestrator worktreeを同期する
|
||||
|
||||
Branch / worktree:
|
||||
- Branch: `ticket/panel-queue-orchestrator-sync`
|
||||
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||
- Implementation commit: `04a3c6e0` (`tui: make panel queue handoff durable`)
|
||||
|
||||
Intent / invariant check:
|
||||
- Panel Queue action is now implemented as root/dev-side Ticket `ready -> queued` mutation plus Queue commit, ff-only sync into the dedicated orchestration worktree, then Orchestrator notify/kick only after sync and Ticket-state verification.
|
||||
- Automatic sync path is constrained to `git -C <orchestration_worktree> merge --ff-only <queue_commit_sha>`.
|
||||
- No automatic merge commit, rebase, stash, patch apply, conflict resolution, or dirty cleanup path was found.
|
||||
- Runtime workspace root / cwd / orchestration worktree / merge target separation remains path-explicit.
|
||||
|
||||
Implementation summary:
|
||||
- `crates/tui/src/multi_pod.rs` changed.
|
||||
- Added Panel Queue preflight for root/orchestration Git identity, branch state, shared common dir, clean worktrees, root Ticket `ready` state, and orchestration-head ancestry.
|
||||
- Added root-side Queue commit creation scoped to the target Ticket record.
|
||||
- Added ff-only orchestration worktree sync and post-sync verification that orchestration HEAD contains the Queue commit and orchestration Ticket backend reads the Ticket as `queued`.
|
||||
- Added focused tests for successful Queue handoff, dirty root blocking, and orchestration branch divergence blocking.
|
||||
|
||||
Coder evidence:
|
||||
- Coder Pod: `yoi-coder-panel-queue-sync`
|
||||
- Commit: `04a3c6e0`
|
||||
- Coder reported `cargo fmt --check`, `cargo check -p tui`, `cargo test -p tui ticket_queue_action -- --nocapture`, `git diff --check`, `nix build .#yoi`, and `./result/bin/yoi ticket doctor` passed. Full `cargo test -p tui` had unrelated existing failures reported by coder.
|
||||
|
||||
Orchestrator validation evidence:
|
||||
- `git diff --check HEAD^..HEAD` — pass
|
||||
- `cargo test -p tui ticket_queue_action -- --nocapture` — pass
|
||||
- `cargo fmt --check` — pass
|
||||
- `cargo check -p tui --all-targets` — pass
|
||||
- `nix build .#yoi` — pass
|
||||
- `./result/bin/yoi ticket doctor` — `doctor: ok`
|
||||
|
||||
Reviewer evidence:
|
||||
- Reviewer Pod: `yoi-reviewer-panel-queue-sync`
|
||||
- Verdict: approve.
|
||||
- Reviewer reran `git diff --check HEAD^..HEAD` and `cargo test -p tui ticket_queue_action -- --nocapture`; both passed.
|
||||
- Reviewer found pre-mutation checks, root-side mutation/commit, ff-only sync, notify-after-verify ordering, path-explicit workspace separation, and failure-message specificity acceptable.
|
||||
- Reviewer did not treat `--no-verify` as a blocker for the automatic target-Ticket-record metadata commit; noted the tradeoff that local commit hooks do not run.
|
||||
|
||||
Blockers fixed / rejected findings:
|
||||
- No reviewer blocker.
|
||||
- `--no-verify` retained as non-blocking per reviewer rationale: hook-side effects are less acceptable for this narrow automatic Ticket metadata commit than skipping local hooks, with pathscope checks and post-validation covering the handoff.
|
||||
|
||||
Residual risks:
|
||||
- If sync or verify fails after the root Queue commit is created, the root/dev side remains durably queued and committed; manual follow-up may be required.
|
||||
- Clean-worktree checks are preflight checks, not a global lock against concurrent filesystem/git changes during the action.
|
||||
- Current root workspace is dirty from separate Panel/user activity (`00001KTVPS6K3` queue attempt and `crates/tui/src/multi_pod.rs` local changes), so merge target safety must be rechecked before any merge.
|
||||
|
||||
Dirty state:
|
||||
- Implementation worktree was checked clean after commit.
|
||||
- Orchestrator worktree has only Ticket dossier changes before this record is committed.
|
||||
- Root/merge-target workspace is dirty and must not be used for merge until understood/cleaned or explicitly authorized.
|
||||
|
||||
Decision needs:
|
||||
- Explicit merge-completion authority is required before merging `ticket/panel-queue-orchestrator-sync` into the recorded merge target, marking final review/done/close, stopping cleanup, or deleting the worktree/branch.
|
||||
- If authority is granted later, recheck branch/worktree/commit identity, independent approval, target dirty state, and rerun post-merge validation before Ticket completion.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: orchestrator at: 2026-06-12T09:12:25Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder/reviewer Pods stopped after merge-ready dossier was recorded.
|
||||
|
||||
- Stopped: `yoi-coder-panel-queue-sync`
|
||||
- Stopped: `yoi-reviewer-panel-queue-sync`
|
||||
- Implementation worktree/branch are retained for explicit merge-completion authority.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -2404,26 +2404,14 @@ async fn dispatch_ticket_action(
|
|||
|
||||
match request.action {
|
||||
NextUserAction::Queue => {
|
||||
if current_ticket.workflow_state != TicketWorkflowState::Ready {
|
||||
return Err(TicketActionError::Stale(
|
||||
"Queue is only valid while state is ready; reload and retry".to_string(),
|
||||
));
|
||||
}
|
||||
backend
|
||||
.queue_ready(
|
||||
TicketIdOrSlug::Id(request.ticket_id.clone()),
|
||||
"workspace-panel",
|
||||
dispatch_panel_queue(
|
||||
&request.workspace_root,
|
||||
&backend,
|
||||
&request.ticket_id,
|
||||
request.orchestrator,
|
||||
current_ticket,
|
||||
)
|
||||
.map_err(|error| TicketActionError::Ticket(error.to_string()))?;
|
||||
let notification =
|
||||
notify_workspace_orchestrator(request.orchestrator, current_ticket).await;
|
||||
Ok(TicketActionOutcome {
|
||||
notice: format!(
|
||||
"Queued Ticket {}; {}. Orchestrator routing is authorized; implementation side effects still require queued -> inprogress acceptance.",
|
||||
current_ticket.id,
|
||||
notification.sentence()
|
||||
),
|
||||
})
|
||||
.await
|
||||
}
|
||||
NextUserAction::Close => unreachable!("Close action is handled before row dispatch"),
|
||||
NextUserAction::Clarify
|
||||
|
|
@ -2439,6 +2427,566 @@ async fn dispatch_ticket_action(
|
|||
}
|
||||
}
|
||||
|
||||
async fn dispatch_panel_queue(
|
||||
workspace_root: &Path,
|
||||
backend: &LocalTicketBackend,
|
||||
ticket_id: &str,
|
||||
orchestrator: Option<OrchestratorNotifyTarget>,
|
||||
current_ticket: &crate::workspace_panel::TicketPanelEntry,
|
||||
) -> Result<TicketActionOutcome, TicketActionError> {
|
||||
if current_ticket.workflow_state != TicketWorkflowState::Ready {
|
||||
return Err(TicketActionError::Stale(format!(
|
||||
"Queue handoff check `root-ticket-state` failed for Ticket {ticket_id} at {}: state is {}, expected ready; reload and retry",
|
||||
backend.root().display(),
|
||||
current_ticket.workflow_state.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
let preflight = prepare_panel_queue_handoff(workspace_root, backend, ticket_id)?;
|
||||
backend
|
||||
.queue_ready(TicketIdOrSlug::Id(ticket_id.to_owned()), "workspace-panel")
|
||||
.map_err(|error| TicketActionError::Ticket(error.to_string()))?;
|
||||
let commit = commit_panel_queue_ticket_record(&preflight)?;
|
||||
let sync = sync_panel_queue_to_orchestration(&preflight, &commit)?;
|
||||
verify_panel_queue_synced(&preflight, &commit)?;
|
||||
let notification = notify_workspace_orchestrator(orchestrator, current_ticket).await;
|
||||
Ok(TicketActionOutcome {
|
||||
notice: format!(
|
||||
"Queued Ticket {}; root Queue commit {}; orchestration sync {}; {}. Orchestrator routing is authorized; implementation side effects still require queued -> inprogress acceptance.",
|
||||
ticket_id,
|
||||
commit.sha,
|
||||
sync.sentence(),
|
||||
notification.sentence()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PanelQueueHandoffPreflight {
|
||||
ticket_id: String,
|
||||
root_top_level: PathBuf,
|
||||
orchestration: OrchestrationWorktreeLayout,
|
||||
ticket_record_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PanelQueueCommit {
|
||||
sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PanelQueueSync {
|
||||
path: PathBuf,
|
||||
branch: String,
|
||||
head: String,
|
||||
}
|
||||
|
||||
impl PanelQueueSync {
|
||||
fn sentence(&self) -> String {
|
||||
format!(
|
||||
"ff-only synced {} ({}) to {}",
|
||||
self.path.display(),
|
||||
self.branch,
|
||||
self.head
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_panel_queue_handoff(
|
||||
workspace_root: &Path,
|
||||
backend: &LocalTicketBackend,
|
||||
ticket_id: &str,
|
||||
) -> Result<PanelQueueHandoffPreflight, TicketActionError> {
|
||||
let root_top_level = git_top_level(workspace_root).map_err(|message| {
|
||||
queue_check_failed("root-worktree-identity", ticket_id, workspace_root, message)
|
||||
})?;
|
||||
let expected_root = workspace_root.canonicalize().map_err(|error| {
|
||||
queue_check_failed(
|
||||
"root-worktree-identity",
|
||||
ticket_id,
|
||||
workspace_root,
|
||||
format!("could not canonicalize root workspace path: {error}"),
|
||||
)
|
||||
})?;
|
||||
if root_top_level != expected_root {
|
||||
return Err(queue_check_failed(
|
||||
"root-worktree-identity",
|
||||
ticket_id,
|
||||
workspace_root,
|
||||
format!(
|
||||
"Git top-level is {}, expected root workspace {}",
|
||||
root_top_level.display(),
|
||||
expected_root.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let root_branch = git_current_branch(&root_top_level).map_err(|message| {
|
||||
queue_check_failed("root-branch", ticket_id, &root_top_level, message)
|
||||
})?;
|
||||
let root_branch = root_branch.ok_or_else(|| {
|
||||
queue_check_failed(
|
||||
"root-branch",
|
||||
ticket_id,
|
||||
&root_top_level,
|
||||
"root workspace is detached; expected merge target branch".to_string(),
|
||||
)
|
||||
})?;
|
||||
ensure_git_effective_user(&root_top_level).map_err(|message| {
|
||||
queue_check_failed("root-git-user", ticket_id, &root_top_level, message)
|
||||
})?;
|
||||
ensure_git_clean("root-clean", ticket_id, &root_top_level)?;
|
||||
|
||||
let orchestration = orchestration_worktree_layout(&root_top_level);
|
||||
if !orchestration.path.exists() {
|
||||
return Err(queue_check_failed(
|
||||
"orchestration-worktree-identity",
|
||||
ticket_id,
|
||||
&orchestration.path,
|
||||
"dedicated orchestration worktree is missing; open the Panel with Orchestrator support before Queue".to_string(),
|
||||
));
|
||||
}
|
||||
validate_existing_orchestration_worktree(&root_top_level, &orchestration).map_err(
|
||||
|message| {
|
||||
queue_check_failed(
|
||||
"orchestration-worktree-identity",
|
||||
ticket_id,
|
||||
&orchestration.path,
|
||||
message,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
ensure_git_clean("orchestration-clean", ticket_id, &orchestration.path)?;
|
||||
|
||||
let orchestration_branch = git_current_branch(&orchestration.path).map_err(|message| {
|
||||
queue_check_failed(
|
||||
"orchestration-branch",
|
||||
ticket_id,
|
||||
&orchestration.path,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
if orchestration_branch.as_deref() != Some(orchestration.branch.as_str()) {
|
||||
return Err(queue_check_failed(
|
||||
"orchestration-branch",
|
||||
ticket_id,
|
||||
&orchestration.path,
|
||||
format!(
|
||||
"orchestration branch is {:?}, expected {}",
|
||||
orchestration_branch, orchestration.branch
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let root_common = git_common_dir(&root_top_level).map_err(|message| {
|
||||
queue_check_failed("shared-common-dir", ticket_id, &root_top_level, message)
|
||||
})?;
|
||||
let orchestration_common = git_common_dir(&orchestration.path).map_err(|message| {
|
||||
queue_check_failed("shared-common-dir", ticket_id, &orchestration.path, message)
|
||||
})?;
|
||||
if root_common != orchestration_common {
|
||||
return Err(queue_check_failed(
|
||||
"shared-common-dir",
|
||||
ticket_id,
|
||||
&orchestration.path,
|
||||
format!(
|
||||
"orchestration common dir {} differs from root common dir {}",
|
||||
orchestration_common.display(),
|
||||
root_common.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
ensure_ticket_state(
|
||||
backend,
|
||||
ticket_id,
|
||||
TicketWorkflowState::Ready,
|
||||
"root-ticket-state",
|
||||
&root_top_level,
|
||||
)?;
|
||||
|
||||
let orchestration_head = git_rev_parse(&orchestration.path, "HEAD").map_err(|message| {
|
||||
queue_check_failed("branch-divergence", ticket_id, &orchestration.path, message)
|
||||
})?;
|
||||
let root_head = git_rev_parse(&root_top_level, "HEAD").map_err(|message| {
|
||||
queue_check_failed("branch-divergence", ticket_id, &root_top_level, message)
|
||||
})?;
|
||||
ensure_git_ancestor(&root_top_level, &orchestration_head, &root_head).map_err(|message| {
|
||||
queue_check_failed(
|
||||
"branch-divergence",
|
||||
ticket_id,
|
||||
&orchestration.path,
|
||||
format!(
|
||||
"orchestration HEAD {orchestration_head} is not an ancestor of root branch {root_branch} HEAD {root_head}: {message}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
let ticket_record_dir = backend.root().join(ticket_id);
|
||||
if !ticket_record_dir.join("item.md").is_file() {
|
||||
return Err(queue_check_failed(
|
||||
"target-ticket-record",
|
||||
ticket_id,
|
||||
&ticket_record_dir,
|
||||
"target Ticket item.md is missing".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(PanelQueueHandoffPreflight {
|
||||
ticket_id: ticket_id.to_string(),
|
||||
root_top_level,
|
||||
orchestration,
|
||||
ticket_record_dir,
|
||||
})
|
||||
}
|
||||
|
||||
fn commit_panel_queue_ticket_record(
|
||||
preflight: &PanelQueueHandoffPreflight,
|
||||
) -> Result<PanelQueueCommit, TicketActionError> {
|
||||
let ticket_rel = path_relative_to_root(
|
||||
&preflight.root_top_level,
|
||||
&preflight.ticket_record_dir,
|
||||
"target-ticket-record",
|
||||
&preflight.ticket_id,
|
||||
)?;
|
||||
let mut add = Command::new("git");
|
||||
add.arg("-C")
|
||||
.arg(&preflight.root_top_level)
|
||||
.arg("add")
|
||||
.arg("--")
|
||||
.arg(&ticket_rel);
|
||||
run_git_command(add, "stage Queue Ticket record").map_err(|message| {
|
||||
queue_check_failed(
|
||||
"queue-commit-stage",
|
||||
&preflight.ticket_id,
|
||||
&preflight.ticket_record_dir,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
|
||||
let staged = git_capture(
|
||||
&preflight.root_top_level,
|
||||
&["diff", "--cached", "--name-only"],
|
||||
"list staged files",
|
||||
)
|
||||
.map_err(|message| {
|
||||
queue_check_failed(
|
||||
"queue-commit-pathscope",
|
||||
&preflight.ticket_id,
|
||||
&preflight.root_top_level,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
let staged_paths = staged
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if staged_paths.is_empty() {
|
||||
return Err(queue_check_failed(
|
||||
"queue-commit-pathscope",
|
||||
&preflight.ticket_id,
|
||||
&preflight.ticket_record_dir,
|
||||
"Queue mutation produced no staged Ticket record changes".to_string(),
|
||||
));
|
||||
}
|
||||
let ticket_rel_string = git_path_string(&ticket_rel);
|
||||
let outside = staged_paths
|
||||
.iter()
|
||||
.find(|path| !git_status_path_is_inside(path, &ticket_rel_string));
|
||||
if let Some(path) = outside {
|
||||
return Err(queue_check_failed(
|
||||
"queue-commit-pathscope",
|
||||
&preflight.ticket_id,
|
||||
&preflight.root_top_level,
|
||||
format!(
|
||||
"staged path {path} is outside target Ticket record {}",
|
||||
ticket_rel.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let message = format!("ticket: queue {}", preflight.ticket_id);
|
||||
let mut commit = Command::new("git");
|
||||
commit
|
||||
.arg("-C")
|
||||
.arg(&preflight.root_top_level)
|
||||
.arg("commit")
|
||||
.arg("--no-verify")
|
||||
.arg("-m")
|
||||
.arg(message)
|
||||
.arg("--")
|
||||
.arg(&ticket_rel);
|
||||
run_git_command(commit, "commit Queue Ticket record").map_err(|message| {
|
||||
queue_check_failed(
|
||||
"queue-commit-create",
|
||||
&preflight.ticket_id,
|
||||
&preflight.root_top_level,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
let sha = git_rev_parse(&preflight.root_top_level, "HEAD").map_err(|message| {
|
||||
queue_check_failed(
|
||||
"queue-commit-create",
|
||||
&preflight.ticket_id,
|
||||
&preflight.root_top_level,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
Ok(PanelQueueCommit { sha })
|
||||
}
|
||||
|
||||
fn sync_panel_queue_to_orchestration(
|
||||
preflight: &PanelQueueHandoffPreflight,
|
||||
commit: &PanelQueueCommit,
|
||||
) -> Result<PanelQueueSync, TicketActionError> {
|
||||
ensure_git_clean(
|
||||
"orchestration-clean-before-sync",
|
||||
&preflight.ticket_id,
|
||||
&preflight.orchestration.path,
|
||||
)?;
|
||||
let mut merge = Command::new("git");
|
||||
merge
|
||||
.arg("-C")
|
||||
.arg(&preflight.orchestration.path)
|
||||
.arg("merge")
|
||||
.arg("--ff-only")
|
||||
.arg(&commit.sha);
|
||||
run_git_command(
|
||||
merge,
|
||||
"ff-only sync Queue commit into orchestration worktree",
|
||||
)
|
||||
.map_err(|message| {
|
||||
queue_check_failed(
|
||||
"orchestration-ff-only-sync",
|
||||
&preflight.ticket_id,
|
||||
&preflight.orchestration.path,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
let head = git_rev_parse(&preflight.orchestration.path, "HEAD").map_err(|message| {
|
||||
queue_check_failed(
|
||||
"orchestration-ff-only-sync",
|
||||
&preflight.ticket_id,
|
||||
&preflight.orchestration.path,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
Ok(PanelQueueSync {
|
||||
path: preflight.orchestration.path.clone(),
|
||||
branch: preflight.orchestration.branch.clone(),
|
||||
head,
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_panel_queue_synced(
|
||||
preflight: &PanelQueueHandoffPreflight,
|
||||
commit: &PanelQueueCommit,
|
||||
) -> Result<(), TicketActionError> {
|
||||
let head = git_rev_parse(&preflight.orchestration.path, "HEAD").map_err(|message| {
|
||||
queue_check_failed(
|
||||
"orchestration-sync-verify",
|
||||
&preflight.ticket_id,
|
||||
&preflight.orchestration.path,
|
||||
message,
|
||||
)
|
||||
})?;
|
||||
ensure_git_ancestor(&preflight.orchestration.path, &commit.sha, &head).map_err(|message| {
|
||||
queue_check_failed(
|
||||
"orchestration-sync-verify",
|
||||
&preflight.ticket_id,
|
||||
&preflight.orchestration.path,
|
||||
format!(
|
||||
"orchestration HEAD {head} does not contain Queue commit {}: {message}",
|
||||
commit.sha
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let config = TicketConfig::load_workspace(&preflight.orchestration.path).map_err(|error| {
|
||||
queue_check_failed(
|
||||
"orchestration-ticket-state",
|
||||
&preflight.ticket_id,
|
||||
&preflight.orchestration.path,
|
||||
error.to_string(),
|
||||
)
|
||||
})?;
|
||||
let backend = LocalTicketBackend::new(config.backend_root())
|
||||
.with_record_language(config.ticket_record_language());
|
||||
ensure_ticket_state(
|
||||
&backend,
|
||||
&preflight.ticket_id,
|
||||
TicketWorkflowState::Queued,
|
||||
"orchestration-ticket-state",
|
||||
&preflight.orchestration.path,
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_ticket_state(
|
||||
backend: &LocalTicketBackend,
|
||||
ticket_id: &str,
|
||||
expected: TicketWorkflowState,
|
||||
check: &'static str,
|
||||
path: &Path,
|
||||
) -> Result<(), TicketActionError> {
|
||||
let ticket = backend
|
||||
.show(TicketIdOrSlug::Id(ticket_id.to_string()))
|
||||
.map_err(|error| queue_check_failed(check, ticket_id, path, error.to_string()))?;
|
||||
if ticket.meta.workflow_state != expected {
|
||||
return Err(queue_check_failed(
|
||||
check,
|
||||
ticket_id,
|
||||
path,
|
||||
format!(
|
||||
"state is {}, expected {}",
|
||||
ticket.meta.workflow_state.as_str(),
|
||||
expected.as_str()
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_git_clean(
|
||||
check: &'static str,
|
||||
ticket_id: &str,
|
||||
path: &Path,
|
||||
) -> Result<(), TicketActionError> {
|
||||
let status = git_status_porcelain(path)
|
||||
.map_err(|message| queue_check_failed(check, ticket_id, path, message))?;
|
||||
if status.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let detail = status.into_iter().take(6).collect::<Vec<_>>().join("; ");
|
||||
Err(queue_check_failed(
|
||||
check,
|
||||
ticket_id,
|
||||
path,
|
||||
format!("worktree is dirty: {detail}"),
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_git_effective_user(path: &Path) -> Result<(), String> {
|
||||
let name = git_capture(path, &["config", "user.name"], "read git user.name")?;
|
||||
let email = git_capture(path, &["config", "user.email"], "read git user.email")?;
|
||||
if name.trim().is_empty() || email.trim().is_empty() {
|
||||
return Err("git user.name and user.email must be configured before the Panel creates a Queue commit".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn git_rev_parse(path: &Path, rev: &str) -> Result<String, String> {
|
||||
git_capture(path, &["rev-parse", rev], "resolve Git revision")
|
||||
}
|
||||
|
||||
fn git_status_porcelain(path: &Path) -> Result<Vec<String>, String> {
|
||||
let output = git_capture(
|
||||
path,
|
||||
&["status", "--porcelain", "--untracked-files=normal"],
|
||||
"read Git status",
|
||||
)?;
|
||||
Ok(output.lines().map(|line| line.to_string()).collect())
|
||||
}
|
||||
|
||||
fn ensure_git_ancestor(path: &Path, ancestor: &str, descendant: &str) -> Result<(), String> {
|
||||
let status = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(path)
|
||||
.arg("merge-base")
|
||||
.arg("--is-ancestor")
|
||||
.arg(ancestor)
|
||||
.arg(descendant)
|
||||
.status()
|
||||
.map_err(|error| format!("could not run git merge-base --is-ancestor: {error}"))?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"git merge-base --is-ancestor {ancestor} {descendant} exited with {status}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn git_capture(path: &Path, args: &[&str], action: &str) -> Result<String, String> {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(path)
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"could not run git to {action} at {}: {error}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
if output.status.success() {
|
||||
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let detail = if stderr.is_empty() { stdout } else { stderr };
|
||||
Err(format!(
|
||||
"git failed to {action} at {}: {detail}",
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
|
||||
fn path_relative_to_root(
|
||||
root: &Path,
|
||||
path: &Path,
|
||||
check: &'static str,
|
||||
ticket_id: &str,
|
||||
) -> Result<PathBuf, TicketActionError> {
|
||||
let canonical = path.canonicalize().map_err(|error| {
|
||||
queue_check_failed(
|
||||
check,
|
||||
ticket_id,
|
||||
path,
|
||||
format!("could not canonicalize path: {error}"),
|
||||
)
|
||||
})?;
|
||||
canonical
|
||||
.strip_prefix(root)
|
||||
.map(PathBuf::from)
|
||||
.map_err(|_| {
|
||||
queue_check_failed(
|
||||
check,
|
||||
ticket_id,
|
||||
path,
|
||||
format!(
|
||||
"path {} is outside root Git top-level {}",
|
||||
canonical.display(),
|
||||
root.display()
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn git_path_string(path: &Path) -> String {
|
||||
path.components()
|
||||
.map(|component| component.as_os_str().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
fn git_status_path_is_inside(path: &str, parent: &str) -> bool {
|
||||
path == parent
|
||||
|| path
|
||||
.strip_prefix(parent)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
}
|
||||
|
||||
fn queue_check_failed(
|
||||
check: &'static str,
|
||||
ticket_id: &str,
|
||||
path: &Path,
|
||||
message: impl Into<String>,
|
||||
) -> TicketActionError {
|
||||
TicketActionError::Stale(format!(
|
||||
"Queue handoff check `{check}` failed for Ticket {ticket_id} at {}: {}",
|
||||
path.display(),
|
||||
message.into()
|
||||
))
|
||||
}
|
||||
|
||||
fn dispatch_panel_close(
|
||||
backend: &LocalTicketBackend,
|
||||
ticket_id: &str,
|
||||
|
|
@ -3675,21 +4223,11 @@ mod tests {
|
|||
fn init_test_repo(root: &Path) {
|
||||
std::fs::create_dir_all(root).unwrap();
|
||||
run_test_git(root, &["init"]).unwrap();
|
||||
run_test_git(root, &["config", "user.email", "test@example.invalid"]).unwrap();
|
||||
run_test_git(root, &["config", "user.name", "Yoi Test"]).unwrap();
|
||||
std::fs::write(root.join("README.md"), "repo").unwrap();
|
||||
run_test_git(root, &["add", "README.md"]).unwrap();
|
||||
run_test_git(
|
||||
root,
|
||||
&[
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"-c",
|
||||
"user.name=Yoi Test",
|
||||
"commit",
|
||||
"-m",
|
||||
"init",
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
run_test_git(root, &["commit", "-m", "init"]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3728,6 +4266,16 @@ mod tests {
|
|||
) -> (TempDir, String, LocalTicketBackend) {
|
||||
let temp = TempDir::new().unwrap();
|
||||
fs::create_dir_all(temp.path().join(".yoi")).unwrap();
|
||||
fs::write(
|
||||
temp.path().join(".gitignore"),
|
||||
".worktree/\n.yoi/tickets/.ticket-backend.lock\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
temp.path().join(".yoi/.gitignore"),
|
||||
"tickets/.ticket-backend.lock\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
temp.path().join(".yoi/ticket.config.toml"),
|
||||
"[backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n",
|
||||
|
|
@ -3746,6 +4294,21 @@ mod tests {
|
|||
ticket_workspace(title, TicketWorkflowState::Ready, |_| {})
|
||||
}
|
||||
|
||||
fn ready_ticket_git_workspace(title: &str) -> (TempDir, String, LocalTicketBackend) {
|
||||
let (temp, ticket_id, backend) = ready_ticket_workspace(title);
|
||||
run_test_git(temp.path(), &["init"]).unwrap();
|
||||
run_test_git(
|
||||
temp.path(),
|
||||
&["config", "user.email", "test@example.invalid"],
|
||||
)
|
||||
.unwrap();
|
||||
run_test_git(temp.path(), &["config", "user.name", "Yoi Test"]).unwrap();
|
||||
run_test_git(temp.path(), &["add", "."]).unwrap();
|
||||
run_test_git(temp.path(), &["commit", "-m", "seed tickets"]).unwrap();
|
||||
ensure_orchestration_worktree(temp.path()).unwrap();
|
||||
(temp, ticket_id, backend)
|
||||
}
|
||||
|
||||
fn done_ticket_workspace(title: &str) -> (TempDir, String, LocalTicketBackend) {
|
||||
ticket_workspace(title, TicketWorkflowState::Done, |_| {})
|
||||
}
|
||||
|
|
@ -3765,14 +4328,23 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn ticket_queue_action_transitions_ready_ticket_and_authorizes_orchestrator_routing() {
|
||||
let (temp, ticket_id, backend) = ready_ticket_workspace("panel-queue");
|
||||
let (temp, ticket_id, backend) = ready_ticket_git_workspace("panel-queue");
|
||||
let root_head_before = git_rev_parse(temp.path(), "HEAD").unwrap();
|
||||
|
||||
let outcome =
|
||||
dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Queue))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let root_head_after = git_rev_parse(temp.path(), "HEAD").unwrap();
|
||||
let layout = orchestration_worktree_layout(temp.path());
|
||||
let orchestration_head = git_rev_parse(&layout.path, "HEAD").unwrap();
|
||||
assert_ne!(root_head_after, root_head_before);
|
||||
assert_eq!(orchestration_head, root_head_after);
|
||||
assert!(outcome.notice.contains("Queued Ticket"));
|
||||
assert!(outcome.notice.contains(&root_head_after));
|
||||
assert!(outcome.notice.contains("root Queue commit"));
|
||||
assert!(outcome.notice.contains("ff-only synced"));
|
||||
assert!(
|
||||
outcome
|
||||
.notice
|
||||
|
|
@ -3780,7 +4352,7 @@ mod tests {
|
|||
);
|
||||
assert!(outcome.notice.contains("queued -> inprogress acceptance"));
|
||||
assert!(!outcome.notice.contains("No implementation was started"));
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap();
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(ticket_id.clone())).unwrap();
|
||||
assert_eq!(ticket.meta.workflow_state, TicketWorkflowState::Queued);
|
||||
assert_eq!(ticket.meta.queued_by.as_deref(), Some("workspace-panel"));
|
||||
assert!(ticket.meta.queued_at.is_some());
|
||||
|
|
@ -3795,6 +4367,56 @@ mod tests {
|
|||
})
|
||||
.expect("queue state_changed event is recorded");
|
||||
assert_eq!(state_change.author.as_deref(), Some("workspace-panel"));
|
||||
let orchestration_backend = LocalTicketBackend::new(layout.path.join(".yoi/tickets"));
|
||||
let orchestration_ticket = orchestration_backend
|
||||
.show(TicketIdOrSlug::Id(ticket_id))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
orchestration_ticket.meta.workflow_state,
|
||||
TicketWorkflowState::Queued
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_queue_action_blocks_dirty_root_without_mutation() {
|
||||
let (temp, ticket_id, backend) = ready_ticket_git_workspace("panel-dirty-root");
|
||||
fs::write(temp.path().join("dirty.txt"), "dirty").unwrap();
|
||||
|
||||
let error =
|
||||
dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Queue))
|
||||
.await
|
||||
.unwrap_err();
|
||||
let message = error.to_string();
|
||||
|
||||
assert!(message.contains("root-clean"));
|
||||
assert!(message.contains(&ticket_id));
|
||||
assert!(message.contains(&temp.path().display().to_string()));
|
||||
assert!(message.contains("dirty.txt"));
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap();
|
||||
assert_eq!(ticket.meta.workflow_state, TicketWorkflowState::Ready);
|
||||
assert!(ticket.meta.queued_by.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_queue_action_blocks_orchestration_branch_divergence() {
|
||||
let (temp, ticket_id, backend) = ready_ticket_git_workspace("panel-diverged");
|
||||
let layout = orchestration_worktree_layout(temp.path());
|
||||
fs::write(layout.path.join("orchestrator-only.txt"), "diverged").unwrap();
|
||||
run_test_git(&layout.path, &["add", "orchestrator-only.txt"]).unwrap();
|
||||
run_test_git(&layout.path, &["commit", "-m", "orchestrator-only"]).unwrap();
|
||||
|
||||
let error =
|
||||
dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Queue))
|
||||
.await
|
||||
.unwrap_err();
|
||||
let message = error.to_string();
|
||||
|
||||
assert!(message.contains("branch-divergence"));
|
||||
assert!(message.contains(&ticket_id));
|
||||
assert!(message.contains(&layout.path.display().to_string()));
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap();
|
||||
assert_eq!(ticket.meta.workflow_state, TicketWorkflowState::Ready);
|
||||
assert!(ticket.meta.queued_by.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user