merge: integrate orchestration branch
This commit is contained in:
commit
65fa4c06ce
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260615-124038-1","ticket_id":"00001KV5D7MG5","kind":"accepted_plan","accepted_plan":{"summary":"Accept Panel orchestration Ticket state overlay work. Implement read-only, source-qualified overlay from configured orchestration worktree with path/branch/repository safety checks and duplicate action gating.","branch":"impl/00001KV5D7MG5-panel-orchestration-overlay","worktree":"/home/hare/Projects/yoi/.worktree/00001KV5D7MG5-panel-orchestration-overlay","role_plan":"Orchestrator creates dedicated implementation worktree and spawns Coder with write scope limited to that worktree. Reviewer will run read-only after implementation report. Work is isolated to Panel/Ticket config/worktree overlay code and current workspace has no active child implementation Pods."},"author":"yoi-orchestrator","at":"2026-06-15T12:40:38Z"}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Panel に orchestration worktree の Ticket state overlay を表示する'
|
||||
state: 'queued'
|
||||
state: 'done'
|
||||
created_at: '2026-06-15T10:29:00Z'
|
||||
updated_at: '2026-06-15T12:39:21Z'
|
||||
updated_at: '2026-06-15T13:05:20Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['panel', 'ticket-state', 'orchestration', 'worktree', 'git-branch', 'read-only-overlay']
|
||||
|
|
|
|||
|
|
@ -13,4 +13,239 @@ LocalTicketBackend によって作成されました。
|
|||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-15T12:40:38Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Panel Queue により routing が明示的に許可され、Ticket は `queued`。
|
||||
- Ticket body / thread / relation / OrchestrationPlan / Orchestrator workspace state / related closed Ticket context を確認した。blocking relation はなく、planning に戻す concrete missing information はない。
|
||||
- `00001KV0X254D` で `.yoi/ticket.config.toml` の orchestration branch 設定が実装済みであり、`00001KV12W2RT` / `00001KV4ZPAD3` で Panel Ticket row / hierarchy 表示の前提が整っている。
|
||||
- Risk は panel / ticket-state / orchestration / worktree / git-branch / read-only-overlay だが、Ticket 本文に safety checks・read-only invariant・fallback・action gating が明記されているため、残る不確実性は implementation tactic に閉じている。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body/thread: overlay source、safety checks、display guidance、action gating、fallback、acceptance criteria、non-goals を確認。
|
||||
- Ticket relations: blocker なし。
|
||||
- OrchestrationPlan: 既存 record なし。
|
||||
- Related context: `00001KV0X254D` closed(orchestration branch config)、`00001KV12W2RT` closed(two-line Ticket row/gate display)。
|
||||
- Current config: `.yoi/ticket.config.toml` を確認。現 checkout には `[orchestration]` section がないため、default `<workspace>/.worktree/orchestration` / branch `orchestration` fallback を扱う必要がある。
|
||||
- Orchestrator workspace: `/home/hare/Projects/yoi/.worktree/orchestration` は clean、`318aa191` 上。
|
||||
- Visible Pods: implementation child Pod なし。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Workspace Panel の canonical current-branch Ticket state を上書きせず、configured orchestration worktree / branch の Ticket state を read-only overlay として読み、source-qualified progress と action gating を表示する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Primary Ticket state authority は current workspace branch の `.yoi/tickets` のまま。
|
||||
- Overlay は read-only。overlay Ticket files を変更せず、current branch Ticket files に自動反映しない。
|
||||
- current branch `state` を overlay state で上書きしない。
|
||||
- Overlay source は expected path / branch / same repository / canonical top-level safety checks 通過時のみ使う。
|
||||
- 誤った worktree / unrelated repository / branch mismatch の Ticket state を表示しない。
|
||||
- Ticket identity join は canonical Ticket id。
|
||||
- Overlay progress is source-qualified(例: `orchestration: inprogress`, `local: queued · orchestration: done`)。
|
||||
- Overlay が current state より進んでいる場合、Queue/Start 等の duplicate action は safety 側で抑止する。
|
||||
- Runtime overlay は merge/review/close authority の代替ではない。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Panel ViewModel が configured orchestration worktree `.yoi/tickets` を read-only overlay として読み込める。
|
||||
- `[orchestration]` config の `branch` / `worktree_dir` / `worktree_name` と defaults を使って expected path/branch を解決する。
|
||||
- current branch state は overlay state で変更されない。
|
||||
- local `queued` + overlay `inprogress` を Panel に overlay progress として表示する。
|
||||
- local `queued` + overlay `done` を `orchestration: done` / merge pending 相当として表示し、Queue/Start を再提示しない。
|
||||
- overlay unavailable / branch mismatch / missing worktree は panic せず current branch 表示へ fallback し、必要なら bounded diagnostic を出す。
|
||||
- Tests cover overlay id join、queued+inprogress、queued+done action gating、branch mismatch ignore、missing worktree fallback、current branch Ticket files non-mutation。
|
||||
|
||||
Implementation latitude:
|
||||
- Overlay-only Ticket の表示有無は実装判断。ただし表示する場合は source を明確にする。
|
||||
- Exact wording / gate/action label は既存 Panel row hierarchy に合わせて調整してよい。
|
||||
- Safety check helper の配置は existing Ticket config / Panel orchestration worktree code に合わせる。
|
||||
- Heavy full doctor を毎 render 必須にしない。
|
||||
|
||||
Escalate if:
|
||||
- Ticket config model に `worktree_dir` / `worktree_name` が存在せず、新 schema の exact authority が必要になる。
|
||||
- same-repository / git-common-dir validation が current abstractions で安全に書けない。
|
||||
- overlay state を読むために current workspace以外の arbitrary path authority が必要になる。
|
||||
- action gating が lifecycle authority と衝突する。
|
||||
- overlay-only Tickets の表示方針が product decision を要する。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p tui workspace_panel --lib` or focused overlay tests。
|
||||
- Relevant ticket config / orchestration worktree tests if config parsing changes。
|
||||
- `cargo check -p tui --all-targets`。
|
||||
- `cargo fmt --check`。
|
||||
- `git diff --check`。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- read-only overlay invariant。
|
||||
- same-repository / branch / canonical path safety checks。
|
||||
- source-qualified display; no state overwrite。
|
||||
- duplicate Queue/Start action suppression when overlay has progressed。
|
||||
- fallback diagnostics boundedness。
|
||||
- no accidental merge/close authority substitution。
|
||||
- row hierarchy and invalid Ticket tolerance regressions。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-15T12:40:45Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Routing decision と accepted implementation plan を記録済み。blocking relation / unresolved OrchestrationPlan blocker はなく、Orchestrator workspace は clean。implementation side effects の前に `queued -> inprogress` acceptance を記録する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KV5D7MG5 at: 2026-06-15T12:57:12Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report from Coder Pod `yoi-coder-00001KV5D7MG5`:
|
||||
|
||||
Commit:
|
||||
- `e0ddbed1 feat: show orchestration ticket overlay in panel`
|
||||
|
||||
Changed files:
|
||||
- `crates/tui/src/workspace_panel.rs`
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
|
||||
Implemented behavior:
|
||||
- Panel ViewModel attempts to read a read-only orchestration worktree Ticket state overlay from configured/default `[orchestration]` settings.
|
||||
- Overlay loading validates:
|
||||
- expected worktree path
|
||||
- expected branch
|
||||
- same Git common-dir/repository
|
||||
- canonical Git top-level equals expected worktree path
|
||||
- Overlay state is joined by canonical Ticket id only.
|
||||
- Local/current branch `.yoi/tickets` remains primary authority and is not mutated or overwritten.
|
||||
- Rows with progressed overlay state show source-qualified state such as:
|
||||
- `local: queued · orchestration: inprogress`
|
||||
- `local: queued · orchestration: done`
|
||||
- Progressed overlay state suppresses duplicate queue/start-like actions:
|
||||
- overlay `inprogress` is treated as active work / read-only progress
|
||||
- overlay `done` is treated as merge/review pending and does not expose queue/start again
|
||||
- Orchestrator work-set derivation accounts for overlay in-progress/done guards to avoid duplicate start routing.
|
||||
|
||||
Tests added/covered:
|
||||
- overlay joins by Ticket id
|
||||
- local `queued` + overlay `inprogress` display/action behavior
|
||||
- local `queued` + overlay `done` merge-pending/action gating
|
||||
- branch mismatch ignored
|
||||
- missing worktree fallback
|
||||
- local Ticket file not mutated by overlay reading
|
||||
|
||||
Validation reported by coder:
|
||||
- Passed: `cargo test -p tui workspace_panel --lib`
|
||||
- Passed: `cargo check -p tui --all-targets`
|
||||
- Passed: `cargo fmt --check`
|
||||
- Passed: `git diff --check`
|
||||
|
||||
Repository status:
|
||||
- Child implementation worktree clean after commit.
|
||||
|
||||
Residual blockers:
|
||||
- None reported.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KV5D7MG5 at: 2026-06-15T13:04:06Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
Evidence checked:
|
||||
- Inspected implementation diff `95abdc8d..e0ddbed1`.
|
||||
- Changed files reviewed:
|
||||
- `crates/tui/src/workspace_panel.rs`
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
- Worktree remained clean after read-only checks.
|
||||
|
||||
Acceptance review:
|
||||
- Current branch remains primary authority:
|
||||
- local Ticket summaries are loaded from the current workspace backend first;
|
||||
- overlay data is joined by canonical Ticket id and stored separately in `PanelTicketInfo.orchestration_overlay`;
|
||||
- `workflow_state` remains local/current-branch state and is not overwritten by overlay.
|
||||
- Overlay is read-only:
|
||||
- overlay loading uses `LocalTicketBackend::list_partial()` against the orchestration worktree ticket root;
|
||||
- no overlay writes or current-branch Ticket mutation path was found;
|
||||
- test coverage includes local Ticket files not being mutated by overlay read.
|
||||
- Safety checks are appropriately conservative:
|
||||
- expected configured/default orchestration path;
|
||||
- path existence/directory check;
|
||||
- canonical top-level comparison;
|
||||
- expected branch check;
|
||||
- same git common-dir check against current workspace.
|
||||
- Branch mismatch and missing worktree fall back safely.
|
||||
- Default orchestration config behavior is covered when no `[orchestration]` section is present.
|
||||
- Canonical Ticket id join is covered; unrelated overlay Tickets do not affect local rows.
|
||||
- Source-qualified display is present, e.g. `local: queued · orchestration: inprogress/done`.
|
||||
- Duplicate Queue/Start suppression is implemented for local `queued` with overlay `inprogress` or `done`.
|
||||
- `multi_pod` queued-work attention also suppresses already-progressed overlay work.
|
||||
- Overlay `done` / `closed` remains merge/review-pending information and does not grant close/merge/lifecycle authority.
|
||||
|
||||
Validation performed by reviewer:
|
||||
- Passed: `git diff --check 95abdc8d..e0ddbed1`
|
||||
- Passed: `cargo fmt --check`
|
||||
- Passed: `git status --short` clean
|
||||
|
||||
Validation not run by reviewer:
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo check -p tui --all-targets`
|
||||
|
||||
Reason: review scope was read-only and Cargo writes build artifacts. Coder-reported passing results for those commands were inspected as evidence.
|
||||
|
||||
Risk assessment:
|
||||
- Low residual risk. The main behavior is ViewModel-tested and safety checks are conservative.
|
||||
- Minor UI risk: compact status column may truncate long source-qualified overlay text, but the full model value and disabled/detail text carry overlay progress context.
|
||||
|
||||
Conclusion:
|
||||
- Approved. No blocker found.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-15T13:05:20Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration report:
|
||||
|
||||
Merged implementation branch into orchestration branch:
|
||||
- branch: `impl/00001KV5D7MG5-panel-orchestration-overlay`
|
||||
- implementation commit: `e0ddbed1 feat: show orchestration ticket overlay in panel`
|
||||
- merge commit: `eeb6986f merge: panel orchestration overlay`
|
||||
|
||||
Reviewer result:
|
||||
- `approve` recorded from `yoi-reviewer-00001KV5D7MG5`.
|
||||
|
||||
Validation in Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||
- Passed: `cargo test -p tui workspace_panel --lib`
|
||||
- 21 passed
|
||||
- Passed: `cargo check -p tui --all-targets`
|
||||
- Passed: `cargo fmt --check`
|
||||
- Passed: `git diff --check`
|
||||
|
||||
Notes:
|
||||
- Overlay is read-only and source-qualified.
|
||||
- Current branch Ticket state remains primary authority and is not overwritten.
|
||||
- Missing worktree / branch mismatch / unrelated repo safety paths are covered by focused tests.
|
||||
- Queue/Start-like duplicate actions are suppressed when overlay shows `inprogress` or `done`.
|
||||
- Orchestrator worktree is clean after validation.
|
||||
|
||||
Cleanup planned:
|
||||
- Stop related coder/reviewer Pods.
|
||||
- Remove only child implementation worktree/branch for this Ticket.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-15T13:05:20Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Reviewer approved, implementation branch merged into the orchestration branch, and focused validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -3211,7 +3211,22 @@ fn derive_orchestrator_work_set(
|
|||
.iter()
|
||||
.filter_map(|row| {
|
||||
let ticket = row.ticket.as_ref()?;
|
||||
if ticket.workflow_state == TicketWorkflowState::InProgress {
|
||||
if let Some(overlay) = ticket
|
||||
.orchestration_overlay
|
||||
.as_ref()
|
||||
.filter(|overlay| overlay.workflow_state == TicketWorkflowState::InProgress)
|
||||
{
|
||||
Some(OrchestratorActiveWorkItem {
|
||||
id: ticket.id.clone(),
|
||||
title: ticket.title.clone(),
|
||||
status: format!(
|
||||
"local: {} · {}: {}",
|
||||
ticket.workflow_state.as_str(),
|
||||
overlay.source,
|
||||
overlay.workflow_state.as_str()
|
||||
),
|
||||
})
|
||||
} else if ticket.workflow_state == TicketWorkflowState::InProgress {
|
||||
Some(OrchestratorActiveWorkItem {
|
||||
id: ticket.id.clone(),
|
||||
title: ticket.title.clone(),
|
||||
|
|
@ -3297,6 +3312,13 @@ fn queued_duplicate_guard(
|
|||
guards.push(format!("related pod/worktree {pod}"));
|
||||
}
|
||||
}
|
||||
if let Some(overlay) = ticket.orchestration_overlay.as_ref() {
|
||||
guards.push(format!(
|
||||
"{} worktree overlay shows state {}",
|
||||
overlay.source,
|
||||
overlay.workflow_state.as_str()
|
||||
));
|
||||
}
|
||||
if guards.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -8893,6 +8915,7 @@ branch = "orchestration/custom-panel"
|
|||
workflow_state: TicketWorkflowState::parse(state)
|
||||
.unwrap_or(TicketWorkflowState::Planning),
|
||||
workflow_state_explicit: true,
|
||||
orchestration_overlay: None,
|
||||
next_action: Some(next_action),
|
||||
updated_at: None,
|
||||
latest_event_kind: Some("implementation_report".to_string()),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use protocol::PodStatus;
|
||||
use ticket::config::{TICKET_CONFIG_RELATIVE_PATH, TicketConfig};
|
||||
use ticket::config::{
|
||||
DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TICKET_CONFIG_RELATIVE_PATH, TicketConfig,
|
||||
TicketOrchestrationConfig,
|
||||
};
|
||||
use ticket::{
|
||||
LocalTicketBackend, TicketBackend, TicketError, TicketEvent, TicketFilter, TicketIdOrSlug,
|
||||
TicketInvalidRecord, TicketMeta, TicketRelationBlocker, TicketSummary, TicketWorkflowState,
|
||||
|
|
@ -242,6 +247,7 @@ pub(crate) struct TicketPanelEntry {
|
|||
pub(crate) priority: String,
|
||||
pub(crate) workflow_state: TicketWorkflowState,
|
||||
pub(crate) workflow_state_explicit: bool,
|
||||
pub(crate) orchestration_overlay: Option<TicketStateOverlay>,
|
||||
pub(crate) next_action: Option<NextUserAction>,
|
||||
pub(crate) updated_at: Option<String>,
|
||||
pub(crate) latest_event_kind: Option<String>,
|
||||
|
|
@ -252,6 +258,12 @@ pub(crate) struct TicketPanelEntry {
|
|||
pub(crate) intake_pods: Vec<TicketAssociatedIntakeEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct TicketStateOverlay {
|
||||
pub(crate) source: String,
|
||||
pub(crate) workflow_state: TicketWorkflowState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct TicketAssociatedIntakeEntry {
|
||||
pub(crate) ticket_id: String,
|
||||
|
|
@ -379,6 +391,27 @@ pub(crate) enum OrchestratorLifecyclePlan {
|
|||
Unavailable(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct OrchestrationTicketOverlay {
|
||||
states: BTreeMap<String, TicketStateOverlay>,
|
||||
diagnostics: Vec<String>,
|
||||
}
|
||||
|
||||
impl OrchestrationTicketOverlay {
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
states: BTreeMap::new(),
|
||||
diagnostics: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct OrchestrationWorktreeLayout {
|
||||
path: PathBuf,
|
||||
branch: String,
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_companion_pod_name(workspace_root: &Path) -> String {
|
||||
let seed = workspace_root
|
||||
.file_name()
|
||||
|
|
@ -543,6 +576,161 @@ pub(crate) fn bounded_panel_diagnostic(message: impl AsRef<str>) -> String {
|
|||
excerpt(&collapsed, 180).unwrap_or_else(|| "unknown diagnostic".to_string())
|
||||
}
|
||||
|
||||
fn load_orchestration_ticket_overlay(
|
||||
workspace_root: &Path,
|
||||
config: &TicketConfig,
|
||||
) -> OrchestrationTicketOverlay {
|
||||
let layout = orchestration_worktree_layout(workspace_root, &config.orchestration);
|
||||
if !layout.path.exists() {
|
||||
return OrchestrationTicketOverlay::empty();
|
||||
}
|
||||
match validate_orchestration_overlay_source(workspace_root, &layout) {
|
||||
Ok(()) => {
|
||||
load_orchestration_ticket_overlay_states(&layout.path, config.ticket_record_language())
|
||||
.unwrap_or_else(|message| OrchestrationTicketOverlay {
|
||||
states: BTreeMap::new(),
|
||||
diagnostics: vec![bounded_panel_diagnostic(format!(
|
||||
"Orchestration Ticket overlay unavailable: {message}"
|
||||
))],
|
||||
})
|
||||
}
|
||||
Err(message) => OrchestrationTicketOverlay {
|
||||
states: BTreeMap::new(),
|
||||
diagnostics: vec![bounded_panel_diagnostic(format!(
|
||||
"Orchestration Ticket overlay unavailable: {message}"
|
||||
))],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn orchestration_worktree_layout(
|
||||
workspace_root: &Path,
|
||||
config: &TicketOrchestrationConfig,
|
||||
) -> OrchestrationWorktreeLayout {
|
||||
OrchestrationWorktreeLayout {
|
||||
path: workspace_root
|
||||
.join(config.worktree_dir())
|
||||
.join(config.worktree_name()),
|
||||
branch: config.effective_branch_name().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_orchestration_ticket_overlay_states(
|
||||
worktree_root: &Path,
|
||||
record_language: Option<&str>,
|
||||
) -> Result<OrchestrationTicketOverlay, String> {
|
||||
let ticket_root = worktree_root.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH);
|
||||
if !ticket_root.is_dir() {
|
||||
return Ok(OrchestrationTicketOverlay {
|
||||
states: BTreeMap::new(),
|
||||
diagnostics: vec![bounded_panel_diagnostic(format!(
|
||||
"Orchestration worktree has no {} directory",
|
||||
DEFAULT_TICKET_BACKEND_RELATIVE_PATH
|
||||
))],
|
||||
});
|
||||
}
|
||||
let backend = LocalTicketBackend::new(ticket_root).with_record_language(record_language);
|
||||
let partial = backend
|
||||
.list_partial(TicketFilter::all())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut states = BTreeMap::new();
|
||||
for summary in partial.tickets {
|
||||
states.insert(
|
||||
summary.id,
|
||||
TicketStateOverlay {
|
||||
source: "orchestration".to_string(),
|
||||
workflow_state: summary.workflow_state,
|
||||
},
|
||||
);
|
||||
}
|
||||
let diagnostics = invalid_ticket_diagnostics(partial.invalid_records.len());
|
||||
Ok(OrchestrationTicketOverlay {
|
||||
states,
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_orchestration_overlay_source(
|
||||
workspace_root: &Path,
|
||||
layout: &OrchestrationWorktreeLayout,
|
||||
) -> Result<(), String> {
|
||||
if !layout.path.exists() {
|
||||
return Err(format!(
|
||||
"expected worktree {} does not exist",
|
||||
layout.path.display()
|
||||
));
|
||||
}
|
||||
if !layout.path.is_dir() {
|
||||
return Err(format!(
|
||||
"expected worktree {} is not a directory",
|
||||
layout.path.display()
|
||||
));
|
||||
}
|
||||
let expected_path = layout
|
||||
.path
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("could not canonicalize expected worktree: {error}"))?;
|
||||
let overlay_top = git_output(&layout.path, &["rev-parse", "--show-toplevel"])?;
|
||||
let overlay_top = PathBuf::from(overlay_top);
|
||||
let overlay_top = overlay_top
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("could not canonicalize overlay git top-level: {error}"))?;
|
||||
if overlay_top != expected_path {
|
||||
return Err(format!(
|
||||
"overlay git top-level {} does not match expected path {}",
|
||||
overlay_top.display(),
|
||||
expected_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let current_common_dir = git_common_dir(workspace_root)?;
|
||||
let overlay_common_dir = git_common_dir(&layout.path)?;
|
||||
if current_common_dir != overlay_common_dir {
|
||||
return Err("expected worktree is from a different git common-dir".to_string());
|
||||
}
|
||||
|
||||
let overlay_branch = git_output(&layout.path, &["branch", "--show-current"])?;
|
||||
if overlay_branch != layout.branch {
|
||||
return Err(format!(
|
||||
"expected branch {} but worktree is on {}",
|
||||
layout.branch, overlay_branch
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn git_common_dir(worktree_root: &Path) -> Result<PathBuf, String> {
|
||||
let common_dir = git_output(worktree_root, &["rev-parse", "--git-common-dir"])?;
|
||||
let common_dir_path = PathBuf::from(common_dir);
|
||||
let absolute = if common_dir_path.is_absolute() {
|
||||
common_dir_path
|
||||
} else {
|
||||
worktree_root.join(common_dir_path)
|
||||
};
|
||||
absolute
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("could not canonicalize git common-dir: {error}"))
|
||||
}
|
||||
|
||||
fn git_output(worktree_root: &Path, args: &[&str]) -> Result<String, String> {
|
||||
let output = Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(worktree_root)
|
||||
.output()
|
||||
.map_err(|error| format!("could not run git {}: {error}", args.join(" ")))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let message = if stderr.is_empty() {
|
||||
format!("git {} exited with {}", args.join(" "), output.status)
|
||||
} else {
|
||||
format!("git {} failed: {stderr}", args.join(" "))
|
||||
};
|
||||
return Err(message);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn build_workspace_panel(
|
||||
workspace_root: &Path,
|
||||
pods: &PodList,
|
||||
|
|
@ -595,10 +783,17 @@ fn build_workspace_panel_with_registry_model(
|
|||
model.header.ticket_root = config.backend_root().to_path_buf();
|
||||
let backend = LocalTicketBackend::new(config.backend_root().to_path_buf())
|
||||
.with_record_language(config.ticket_record_language());
|
||||
match build_ticket_rows(&backend, pods, registry) {
|
||||
let orchestration_overlay =
|
||||
load_orchestration_ticket_overlay(workspace_root, &config);
|
||||
match build_ticket_rows(&backend, pods, registry, &orchestration_overlay.states)
|
||||
{
|
||||
Ok(ticket_rows) => {
|
||||
model.rows.extend(ticket_rows.rows);
|
||||
model.header.diagnostics.extend(ticket_rows.diagnostics);
|
||||
model
|
||||
.header
|
||||
.diagnostics
|
||||
.extend(orchestration_overlay.diagnostics);
|
||||
}
|
||||
Err(error) => {
|
||||
model
|
||||
|
|
@ -652,6 +847,7 @@ pub(crate) fn build_current_ticket_row(
|
|||
&ticket.relations.blockers,
|
||||
pods,
|
||||
®istry,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -683,6 +879,7 @@ fn build_ticket_rows(
|
|||
backend: &LocalTicketBackend,
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
orchestration_overlay: &BTreeMap<String, TicketStateOverlay>,
|
||||
) -> ticket::Result<TicketRowsBuild> {
|
||||
let partial = backend.list_partial(TicketFilter::all())?;
|
||||
let mut ticket_rows = Vec::new();
|
||||
|
|
@ -701,12 +898,14 @@ fn build_ticket_rows(
|
|||
if current_ticket_invalid {
|
||||
continue;
|
||||
}
|
||||
let overlay = orchestration_overlay.get(&summary.id);
|
||||
ticket_rows.push(ticket_row(
|
||||
summary,
|
||||
&ticket.ticket.events,
|
||||
&ticket.ticket.relations.blockers,
|
||||
pods,
|
||||
registry,
|
||||
overlay,
|
||||
));
|
||||
}
|
||||
Err(_) => invalid_records.push(TicketInvalidRecord {
|
||||
|
|
@ -802,6 +1001,7 @@ fn ticket_row(
|
|||
relation_blockers: &[TicketRelationBlocker],
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
orchestration_overlay: Option<&TicketStateOverlay>,
|
||||
) -> PanelRow {
|
||||
let local_claim = local_claim_for_ticket(&summary, pods, registry);
|
||||
let intake_pods =
|
||||
|
|
@ -815,14 +1015,24 @@ fn ticket_row(
|
|||
related_pods.push(pod_name);
|
||||
}
|
||||
}
|
||||
let derived = derive_ticket_state(&summary, relation_blockers);
|
||||
let visible_overlay = orchestration_overlay
|
||||
.filter(|overlay| {
|
||||
overlay_state_has_progressed(summary.workflow_state, overlay.workflow_state)
|
||||
})
|
||||
.cloned();
|
||||
let mut derived = derive_ticket_state(&summary, relation_blockers);
|
||||
if let Some(overlay) = visible_overlay.as_ref() {
|
||||
apply_orchestration_overlay_to_derived(&mut derived, summary.workflow_state, overlay);
|
||||
}
|
||||
let latest_event = events.last();
|
||||
let state_display = ticket_state_display(summary.workflow_state, visible_overlay.as_ref());
|
||||
let entry = TicketPanelEntry {
|
||||
id: summary.id.clone(),
|
||||
title: summary.title.clone(),
|
||||
priority: summary.priority.clone(),
|
||||
workflow_state: summary.workflow_state,
|
||||
workflow_state_explicit: summary.workflow_state_explicit,
|
||||
orchestration_overlay: visible_overlay,
|
||||
next_action: derived.action,
|
||||
updated_at: summary.updated_at.clone(),
|
||||
latest_event_kind: latest_event.map(|event| event.kind.as_str().to_string()),
|
||||
|
|
@ -838,7 +1048,7 @@ fn ticket_row(
|
|||
kind: derived.kind,
|
||||
title: summary.title,
|
||||
subtitle,
|
||||
status: summary.workflow_state.as_str().to_string(),
|
||||
status: state_display,
|
||||
priority: derived.priority,
|
||||
next_action: derived.action,
|
||||
ticket: Some(entry),
|
||||
|
|
@ -858,6 +1068,76 @@ struct DerivedTicketState {
|
|||
blocked_reason: Option<String>,
|
||||
}
|
||||
|
||||
fn workflow_state_progress_rank(state: TicketWorkflowState) -> u8 {
|
||||
match state {
|
||||
TicketWorkflowState::Planning => 0,
|
||||
TicketWorkflowState::Ready => 1,
|
||||
TicketWorkflowState::Queued => 2,
|
||||
TicketWorkflowState::InProgress => 3,
|
||||
TicketWorkflowState::Done => 4,
|
||||
TicketWorkflowState::Closed => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_state_has_progressed(local: TicketWorkflowState, overlay: TicketWorkflowState) -> bool {
|
||||
workflow_state_progress_rank(overlay) > workflow_state_progress_rank(local)
|
||||
}
|
||||
|
||||
fn ticket_state_display(
|
||||
local: TicketWorkflowState,
|
||||
overlay: Option<&TicketStateOverlay>,
|
||||
) -> String {
|
||||
match overlay {
|
||||
Some(overlay) => format!(
|
||||
"local: {} · {}: {}",
|
||||
local.as_str(),
|
||||
overlay.source,
|
||||
overlay.workflow_state.as_str()
|
||||
),
|
||||
None => local.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_orchestration_overlay_to_derived(
|
||||
derived: &mut DerivedTicketState,
|
||||
local: TicketWorkflowState,
|
||||
overlay: &TicketStateOverlay,
|
||||
) {
|
||||
derived.action = Some(NextUserAction::Wait);
|
||||
let overlay_state = overlay.workflow_state.as_str();
|
||||
match overlay.workflow_state {
|
||||
TicketWorkflowState::Done | TicketWorkflowState::Closed => {
|
||||
derived.kind = PanelRowKind::Review;
|
||||
derived.priority = ActionPriority::Background;
|
||||
derived.disabled_reason = Some(format!(
|
||||
"{} worktree overlay shows Ticket state {overlay_state}; local state remains {} until merge/review/close authority updates the current branch.",
|
||||
overlay.source,
|
||||
local.as_str()
|
||||
));
|
||||
derived.key_hint = Some(format!(
|
||||
"Merge pending: local: {} · {}: {overlay_state}",
|
||||
local.as_str(),
|
||||
overlay.source
|
||||
));
|
||||
}
|
||||
TicketWorkflowState::InProgress | TicketWorkflowState::Queued => {
|
||||
derived.kind = PanelRowKind::ActiveWork;
|
||||
derived.priority = ActionPriority::ActiveWork;
|
||||
derived.disabled_reason = Some(format!(
|
||||
"{} worktree overlay shows Ticket state {overlay_state}; local state remains {} and duplicate queue/start actions are suppressed.",
|
||||
overlay.source,
|
||||
local.as_str()
|
||||
));
|
||||
derived.key_hint = Some(format!(
|
||||
"Progress overlay: local: {} · {}: {overlay_state}",
|
||||
local.as_str(),
|
||||
overlay.source
|
||||
));
|
||||
}
|
||||
TicketWorkflowState::Planning | TicketWorkflowState::Ready => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_ticket_state(
|
||||
summary: &TicketSummary,
|
||||
relation_blockers: &[TicketRelationBlocker],
|
||||
|
|
@ -1099,7 +1379,11 @@ pub(crate) fn local_claim_status_for_pod(pod_name: &str, pods: &PodList) -> Tick
|
|||
}
|
||||
|
||||
fn ticket_subtitle(entry: &TicketPanelEntry) -> Option<String> {
|
||||
let mut parts = vec![format!("{} · {}", entry.id, entry.workflow_state.as_str())];
|
||||
let mut parts = vec![format!(
|
||||
"{} · {}",
|
||||
entry.id,
|
||||
ticket_state_display(entry.workflow_state, entry.orchestration_overlay.as_ref())
|
||||
)];
|
||||
if let Some(claim) = entry.local_claim.as_ref() {
|
||||
parts.push(format!(
|
||||
"claim: {} ({})",
|
||||
|
|
@ -1212,7 +1496,10 @@ mod tests {
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::TempDir;
|
||||
use ticket::{NewTicket, NewTicketRelation, TicketRelationKind, TicketWorkflowState};
|
||||
use ticket::{
|
||||
NewTicket, NewTicketRelation, TicketIdOrSlug, TicketRelationKind, TicketStateChange,
|
||||
TicketWorkflowState,
|
||||
};
|
||||
|
||||
fn empty_pods() -> PodList {
|
||||
PodList::from_sources(
|
||||
|
|
@ -1229,9 +1516,17 @@ mod tests {
|
|||
title: &str,
|
||||
configure: impl FnOnce(&mut NewTicket),
|
||||
) {
|
||||
create_ticket_with_id(backend, title, configure);
|
||||
}
|
||||
|
||||
fn create_ticket_with_id(
|
||||
backend: &LocalTicketBackend,
|
||||
title: &str,
|
||||
configure: impl FnOnce(&mut NewTicket),
|
||||
) -> String {
|
||||
let mut input = NewTicket::new(title);
|
||||
configure(&mut input);
|
||||
backend.create(input).unwrap();
|
||||
backend.create(input).unwrap().id
|
||||
}
|
||||
|
||||
fn write_ticket_config(workspace_root: &Path) {
|
||||
|
|
@ -1244,6 +1539,111 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
fn run_git(workspace_root: &Path, args: &[&str]) {
|
||||
let output = Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(workspace_root)
|
||||
.output()
|
||||
.unwrap_or_else(|error| panic!("failed to run git {args:?}: {error}"));
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {args:?} failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn init_git_repo(workspace_root: &Path) {
|
||||
run_git(workspace_root, &["init", "--initial-branch", "main"]);
|
||||
run_git(workspace_root, &["config", "user.name", "Panel Test"]);
|
||||
run_git(
|
||||
workspace_root,
|
||||
&["config", "user.email", "panel-test@example.invalid"],
|
||||
);
|
||||
fs::write(workspace_root.join("README.md"), "panel test\n").unwrap();
|
||||
run_git(workspace_root, &["add", "README.md"]);
|
||||
run_git(workspace_root, &["commit", "-m", "init"]);
|
||||
}
|
||||
|
||||
fn add_orchestration_worktree(workspace_root: &Path, branch: &str) -> PathBuf {
|
||||
let orchestration_root = workspace_root.join(".worktree/orchestration");
|
||||
fs::create_dir_all(orchestration_root.parent().unwrap()).unwrap();
|
||||
run_git(
|
||||
workspace_root,
|
||||
&[
|
||||
"worktree",
|
||||
"add",
|
||||
"-b",
|
||||
branch,
|
||||
orchestration_root.to_str().unwrap(),
|
||||
"HEAD",
|
||||
],
|
||||
);
|
||||
orchestration_root
|
||||
}
|
||||
|
||||
fn copy_ticket_to_overlay(workspace_root: &Path, orchestration_root: &Path, id: &str) {
|
||||
let local_ticket_dir = workspace_root.join(".yoi/tickets").join(id);
|
||||
let overlay_ticket_dir = orchestration_root.join(".yoi/tickets").join(id);
|
||||
fs::create_dir_all(overlay_ticket_dir.parent().unwrap()).unwrap();
|
||||
fs::create_dir_all(&overlay_ticket_dir).unwrap();
|
||||
fs::copy(
|
||||
local_ticket_dir.join("item.md"),
|
||||
overlay_ticket_dir.join("item.md"),
|
||||
)
|
||||
.unwrap();
|
||||
let local_thread = local_ticket_dir.join("thread.md");
|
||||
if local_thread.exists() {
|
||||
fs::copy(local_thread, overlay_ticket_dir.join("thread.md")).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ticket_state(backend: &LocalTicketBackend, id: &str, state: TicketWorkflowState) {
|
||||
loop {
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(id.to_string())).unwrap();
|
||||
if ticket.meta.workflow_state == state {
|
||||
break;
|
||||
}
|
||||
let next = match (ticket.meta.workflow_state, state) {
|
||||
(TicketWorkflowState::Queued, TicketWorkflowState::InProgress)
|
||||
| (TicketWorkflowState::Queued, TicketWorkflowState::Done) => {
|
||||
TicketWorkflowState::InProgress
|
||||
}
|
||||
(TicketWorkflowState::InProgress, TicketWorkflowState::Done) => {
|
||||
TicketWorkflowState::Done
|
||||
}
|
||||
(from, to) => panic!("unsupported test transition {from} -> {to}"),
|
||||
};
|
||||
backend
|
||||
.set_workflow_state(
|
||||
TicketIdOrSlug::Id(id.to_string()),
|
||||
TicketStateChange::new(
|
||||
ticket.meta.workflow_state.as_str(),
|
||||
next.as_str(),
|
||||
"test",
|
||||
format!("test state -> {}", next.as_str()),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn ticket_row_by_title<'a>(model: &'a WorkspacePanelViewModel, title: &str) -> &'a PanelRow {
|
||||
model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == title)
|
||||
.unwrap_or_else(|| panic!("missing row for {title}"))
|
||||
}
|
||||
|
||||
fn status_contains(row: &PanelRow, needle: &str) {
|
||||
assert!(
|
||||
row.status.contains(needle),
|
||||
"status {:?} did not contain {:?}",
|
||||
row.status,
|
||||
needle
|
||||
);
|
||||
}
|
||||
|
||||
fn live_pods(names: &[&str]) -> PodList {
|
||||
PodList::from_sources(
|
||||
crate::pod_list::PodVisibilitySource::ResumePicker,
|
||||
|
|
@ -1308,6 +1708,151 @@ mod tests {
|
|||
assert_eq!(row.next_action, Some(NextUserAction::Queue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_joins_orchestration_overlay_by_ticket_id() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
init_git_repo(temp.path());
|
||||
write_ticket_config(temp.path());
|
||||
let orchestration_root = add_orchestration_worktree(temp.path(), "orchestration");
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let id = create_ticket_with_id(&backend, "Overlay Match", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
create_ticket(&backend, "Local Only", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
copy_ticket_to_overlay(temp.path(), &orchestration_root, &id);
|
||||
let overlay_backend = LocalTicketBackend::new(orchestration_root.join(".yoi/tickets"));
|
||||
set_ticket_state(&overlay_backend, &id, TicketWorkflowState::InProgress);
|
||||
create_ticket(&overlay_backend, "Overlay Only", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Done);
|
||||
});
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
|
||||
let matched = ticket_row_by_title(&model, "Overlay Match");
|
||||
status_contains(matched, "local: queued");
|
||||
status_contains(matched, "orchestration: inprogress");
|
||||
assert_eq!(
|
||||
matched.ticket.as_ref().unwrap().workflow_state,
|
||||
TicketWorkflowState::Queued
|
||||
);
|
||||
assert_eq!(
|
||||
matched
|
||||
.ticket
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.orchestration_overlay
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.workflow_state,
|
||||
TicketWorkflowState::InProgress
|
||||
);
|
||||
assert_eq!(ticket_row_by_title(&model, "Local Only").status, "queued");
|
||||
assert!(model.rows.iter().all(|row| row.title != "Overlay Only"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_displays_queued_plus_orchestration_inprogress_without_mutating_local_ticket()
|
||||
{
|
||||
let temp = TempDir::new().unwrap();
|
||||
init_git_repo(temp.path());
|
||||
write_ticket_config(temp.path());
|
||||
let orchestration_root = add_orchestration_worktree(temp.path(), "orchestration");
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let id = create_ticket_with_id(&backend, "Overlay In Progress", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
copy_ticket_to_overlay(temp.path(), &orchestration_root, &id);
|
||||
let overlay_backend = LocalTicketBackend::new(orchestration_root.join(".yoi/tickets"));
|
||||
set_ticket_state(&overlay_backend, &id, TicketWorkflowState::InProgress);
|
||||
let local_item = temp.path().join(".yoi/tickets").join(&id).join("item.md");
|
||||
let before = fs::read_to_string(&local_item).unwrap();
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
|
||||
let row = ticket_row_by_title(&model, "Overlay In Progress");
|
||||
status_contains(row, "local: queued");
|
||||
status_contains(row, "orchestration: inprogress");
|
||||
assert_eq!(row.next_action, Some(NextUserAction::Wait));
|
||||
assert_eq!(row.kind, PanelRowKind::ActiveWork);
|
||||
assert_eq!(fs::read_to_string(&local_item).unwrap(), before);
|
||||
let local_ticket = backend.show(TicketIdOrSlug::Id(id)).unwrap();
|
||||
assert_eq!(
|
||||
local_ticket.meta.workflow_state,
|
||||
TicketWorkflowState::Queued
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_displays_queued_plus_orchestration_done_as_merge_pending_without_queue() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
init_git_repo(temp.path());
|
||||
write_ticket_config(temp.path());
|
||||
let orchestration_root = add_orchestration_worktree(temp.path(), "orchestration");
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let id = create_ticket_with_id(&backend, "Overlay Done", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
copy_ticket_to_overlay(temp.path(), &orchestration_root, &id);
|
||||
let overlay_backend = LocalTicketBackend::new(orchestration_root.join(".yoi/tickets"));
|
||||
set_ticket_state(&overlay_backend, &id, TicketWorkflowState::Done);
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
|
||||
let row = ticket_row_by_title(&model, "Overlay Done");
|
||||
status_contains(row, "local: queued");
|
||||
status_contains(row, "orchestration: done");
|
||||
assert_eq!(row.kind, PanelRowKind::Review);
|
||||
assert_eq!(row.next_action, Some(NextUserAction::Wait));
|
||||
assert_ne!(row.next_action, Some(NextUserAction::Queue));
|
||||
assert!(
|
||||
row.disabled_reason
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.contains("merge/review/close authority")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_ignores_orchestration_overlay_on_branch_mismatch() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
init_git_repo(temp.path());
|
||||
write_ticket_config(temp.path());
|
||||
let orchestration_root = add_orchestration_worktree(temp.path(), "other-branch");
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let id = create_ticket_with_id(&backend, "Branch Mismatch", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
copy_ticket_to_overlay(temp.path(), &orchestration_root, &id);
|
||||
let overlay_backend = LocalTicketBackend::new(orchestration_root.join(".yoi/tickets"));
|
||||
set_ticket_state(&overlay_backend, &id, TicketWorkflowState::Done);
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
|
||||
let row = ticket_row_by_title(&model, "Branch Mismatch");
|
||||
assert_eq!(row.status, "queued");
|
||||
assert!(row.ticket.as_ref().unwrap().orchestration_overlay.is_none());
|
||||
let diagnostics = model.header.diagnostics.join("\n");
|
||||
assert!(diagnostics.contains("expected branch orchestration"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_falls_back_when_orchestration_worktree_is_missing() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_ticket_config(temp.path());
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
create_ticket(&backend, "Missing Overlay", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
|
||||
let row = ticket_row_by_title(&model, "Missing Overlay");
|
||||
assert_eq!(row.status, "queued");
|
||||
assert!(row.ticket.as_ref().unwrap().orchestration_overlay.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_keeps_valid_ticket_actions_with_invalid_records() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user