chore: record panel followups

This commit is contained in:
Keisuke Hirata 2026-06-18 22:41:54 +09:00
parent b6685af3ae
commit 8cbade818f
No known key found for this signature in database
14 changed files with 411 additions and 60 deletions

View File

@ -1,8 +1,8 @@
--- ---
title: "Workspace panel Companion interface" title: "Workspace panel Companion interface"
state: "planning" state: 'closed'
created_at: "2026-06-07T00:16:51Z" created_at: "2026-06-07T00:16:51Z"
updated_at: "2026-06-07T03:13:01Z" updated_at: '2026-06-18T13:06:31Z'
--- ---
## Background ## Background

View File

@ -0,0 +1,10 @@
Closed as completed by child Tickets.
The original Workspace Panel Companion interface plan has been implemented through more specific work:
- direct selected-Pod send was removed from the Panel composer path;
- Panel composer routing now targets the workspace Companion and Ticket Intake explicitly;
- workspace Companion Pod lifecycle restore/spawn/observe behavior is implemented;
- local role/session registry and Ticket claim handling were added for Panel-launched role sessions;
- project role Profile feature defaults limit Companion authority and keep Ticket orchestration / Pods / Task disabled for Companion by default.
The remaining work in this area should be tracked as targeted follow-up Tickets rather than keeping this umbrella planning Ticket open.

View File

@ -74,4 +74,31 @@ Companion work is useful but not required for near-term panel operation. The pan
Decision: downgrade Companion-related follow-up priority to P2 so near-term focus can stay on Ticket role config strictness/init, Orchestrator queue automation, and workflow/compaction reliability. Decision: downgrade Companion-related follow-up priority to P2 so near-term focus can stay on Ticket role config strictness/init, Orchestrator queue automation, and workflow/compaction reliability.
---
<!-- event: state_changed author: hare at: 2026-06-18T13:06:31Z from: planning to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-18T13:06:31Z status: closed -->
## 完了
Closed as completed by child Tickets.
The original Workspace Panel Companion interface plan has been implemented through more specific work:
- direct selected-Pod send was removed from the Panel composer path;
- Panel composer routing now targets the workspace Companion and Ticket Intake explicitly;
- workspace Companion Pod lifecycle restore/spawn/observe behavior is implemented;
- local role/session registry and Ticket claim handling were added for Panel-launched role sessions;
- project role Profile feature defaults limit Companion authority and keep Ticket orchestration / Pods / Task disabled for Companion by default.
The remaining work in this area should be tracked as targeted follow-up Tickets rather than keeping this umbrella planning Ticket open.
--- ---

View File

@ -1,8 +1,8 @@
--- ---
title: 'Plugin: package discovery and explicit enablement resolver' title: 'Plugin: package discovery and explicit enablement resolver'
state: 'done' state: 'closed'
created_at: '2026-06-15T13:40:15Z' created_at: '2026-06-15T13:40:15Z'
updated_at: '2026-06-15T15:30:00Z' updated_at: '2026-06-18T12:22:04Z'
assignee: null assignee: null
readiness: 'implementation_ready' readiness: 'implementation_ready'
risk_flags: ['plugin', 'package-loading', 'discovery', 'enablement', 'capability-boundary', 'startup-restore'] risk_flags: ['plugin', 'package-loading', 'discovery', 'enablement', 'capability-boundary', 'startup-restore']

View File

@ -0,0 +1,3 @@
Ticket `00001KV5R5V2S` (`Plugin: package discovery and explicit enablement resolver`) はすでに `state: done` に到達していたため、workspace Panel から close しました。
この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。

View File

@ -524,4 +524,24 @@ Cleanup planned:
Reviewer approved after requested fixes, implementation branch merged into the orchestration branch, and focused plus packaging validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch. Reviewer approved after requested fixes, implementation branch merged into the orchestration branch, and focused plus packaging validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch.
---
<!-- event: state_changed author: hare at: 2026-06-18T12:22:04Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-18T12:22:04Z status: closed -->
## 完了
Ticket `00001KV5R5V2S` (`Plugin: package discovery and explicit enablement resolver`) はすでに `state: done` に到達していたため、workspace Panel から close しました。
この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。
--- ---

View File

@ -2,7 +2,7 @@
title: 'Panel startup latency E2E を一覧データ描画完了基準に修正する' title: 'Panel startup latency E2E を一覧データ描画完了基準に修正する'
state: 'done' state: 'done'
created_at: '2026-06-15T16:44:06Z' created_at: '2026-06-15T16:44:06Z'
updated_at: '2026-06-18T12:25:14Z' updated_at: '2026-06-18T13:30:51Z'
assignee: null assignee: null
readiness: 'implementation_ready' readiness: 'implementation_ready'
risk_flags: ['panel', 'e2e', 'startup-latency', 'readiness-metric', 'ticket-list-rendering'] risk_flags: ['panel', 'e2e', 'startup-latency', 'readiness-metric', 'ticket-list-rendering']

View File

@ -281,4 +281,17 @@ Cleanup planned:
Reviewer approved, implementation branch merged into the orchestration branch, and E2E-focused validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch. Reviewer approved, implementation branch merged into the orchestration branch, and E2E-focused validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch.
---
<!-- event: review author: hare at: 2026-06-18T13:30:51Z status: request_changes -->
## Review: request changes
Request changes.
The current result still does not answer the user-facing latency problem. The problematic latency is the time from launching `yoi panel` / pressing Enter to seeing the actual workspace dashboard content. The current E2E measures a direct subprocess spawn to one concrete fixture Ticket row appearing in `rows_rendered`; it does not require the dashboard content to be complete from the user's perspective, and it does not reproduce or attribute the clearly long live-workspace delay.
Do not treat fixture first-frame or single-row readiness numbers as evidence that no improvement is needed. The acceptance criterion must be strengthened to a user-visible dashboard-content-ready point and paired with slow-source attribution/improvement for the live-like Panel startup path.
--- ---

View File

@ -0,0 +1,29 @@
{
"version": 1,
"relations": [
{
"ticket_id": "00001KVDETSN6",
"kind": "related",
"target": "00001KV5D7MG5",
"note": "Dashboard content-ready fixture should include orchestration overlay state.",
"author": "yoi ticket",
"at": "2026-06-18T13:31:43Z"
},
{
"ticket_id": "00001KVDETSN6",
"kind": "related",
"target": "00001KV5MRH6D",
"note": "Follows up Panel startup latency E2E work.",
"author": "yoi ticket",
"at": "2026-06-18T13:31:43Z"
},
{
"ticket_id": "00001KVDETSN6",
"kind": "related",
"target": "00001KV62PF32",
"note": "Supersedes the insufficient single-row rows-ready E2E with user-visible dashboard content-ready measurement.",
"author": "yoi ticket",
"at": "2026-06-18T13:31:43Z"
}
]
}

View File

@ -0,0 +1,103 @@
---
title: 'Panel startup latency をユーザー目線の dashboard content ready 基準で計測・改善する'
state: 'ready'
created_at: '2026-06-18T13:30:51Z'
updated_at: '2026-06-18T13:31:43Z'
assignee: null
readiness: 'implementation_ready'
risk_flags: ['panel', 'e2e', 'startup-latency', 'user-visible-readiness', 'dashboard-content', 'profiling']
---
## Background
ユーザーが問題にしている `yoi panel` startup latency は、first frame や fixture の単一 Ticket row が `rows_rendered` に出るまでではなく、**ユーザーが `yoi panel` を起動してから、workspace dashboard として実際に使えるコンテンツが画面に揃って見えるまで**の時間である。
既存の `00001KV62PF32``panel_ready` / first frame と単一 fixture Ticket row readiness の混同を修正したが、まだ以下の点で不十分だった。
- direct subprocess spawn から単一 fixture Ticket row の `rows_rendered` までを測っているだけで、workspace dashboard 全体の content ready ではない。
- live workspace でユーザーが体感している明らかに長い遅延を再現・属性分解していない。
- fixture 上の約 120ms rows-ready をもって「追加改善不要」と判断してしまうと、ユーザー視点の問題を取り逃がす。
この Ticket では、Panel startup latency の主基準を user-visible dashboard content ready に置き直し、遅延源を計測・改善する。
## Definitions
- `panel_first_frame`: 初回 visible draw。loading / empty frame でもよい補助 metric。
- `fixture_single_row_ready`: 具体的な fixture Ticket row が `rows_rendered` に現れる補助 metric。
- `dashboard_content_ready`: ユーザーが workspace dashboard として必要な主要コンテンツが揃い、実際に画面へ描画された状態。この Ticket の主 metric。
`dashboard_content_ready` は少なくとも以下を含む。
- Ticket rows が fixture / live-like workspace の期待データと一致している。
- id
- title
- state/status
- row kind
- primary action / disabled reason where relevant
- Pod / Companion / Orchestrator 関連 row または status が、fixture / live-like workspace の期待状態と一致している。
- orchestration overlay を含む fixture では、local / orchestration state が表示上も期待通り反映されている。
- loading / empty / partial single-row render だけでは ready とみなさない。
## Requirements
- E2E / harness の readiness event または helper を追加・修正し、`dashboard_content_ready` を測れるようにする。
- first frame / single-row readiness とは別 metric にする。
- event 名・test 名・log 出力から意味が誤解されないようにする。
- 測定開始点は、ユーザーの `yoi panel` 起動に十分近いものにする。
- 基本は `Command::spawn` 直前からでよい。
- interactive shell 入力まで含めない場合は、その範囲を test/report に明記する。
- Fixture を live-like に強化する。
- 複数 Ticket state を含める。
- Pod metadata / Companion / Orchestrator 表示を含める。
- orchestration overlay を含める。
- 必要に応じて stale socket / slow observation / many Ticket records など、実遅延の候補を再現する fixture を追加する。
- `dashboard_content_ready` は単なる `rows.len() >= N` や単一 Ticket row match だけで通さない。
- expected dashboard snapshot / expected row set として比較する。
- 欠落 row、wrong status、wrong action、overlay 未反映を fail にする。
- Live workspace 相当の遅延源を属性分解する。
- Ticket scan / parsing
- orchestration overlay worktree validation / read
- Pod metadata scan
- socket/status probing
- Companion / Orchestrator lifecycle observation
- role session / local claim scan
- git worktree / branch checks
- 明らかに長い遅延がある場合は改善する。
- UI 初期化を content-ready 待ちで止めないだけでは不十分。
- 実コンテンツが揃うまでの経路自体を短くする。
- slow source を lazy / bounded / parallel / cached / timeout-shortened にできる場合は実装する。
- Before / after の実測値を implementation report に記録する。
- first frame
- dashboard content ready
- slow-source breakdown
- fixture 条件 / live-like 条件
- 測定で改善不要と判断する場合でも、ユーザーが見ている長い live latency がなぜ再現しないか、またはどの範囲外かを明示する。
## Acceptance criteria
- E2E が `dashboard_content_ready` を主 startup latency metric として測る。
- `panel_first_frame` または単一 Ticket row readiness だけでは、この Ticket の主 E2E は通らない。
- Expected dashboard snapshot に含まれる Ticket / Pod / Companion / Orchestrator / overlay 要素が揃って描画された時点を ready として扱う。
- Missing row / wrong state / missing overlay / missing action label の fixture では ready 判定が fail する。
- User-visible dashboard content ready の before / after 実測値が記録される。
- 遅延源の breakdown が記録され、主要 slow source に対して具体的な改善または明示的な non-action rationale がある。
- Live-like fixture または current workspace に近い条件で、ユーザー体感の長い遅延を取り逃がさない。
- Existing Panel behavior に regression がない。
- row selection
- composer target
- Queue action
- orchestration overlay display
- Validation: relevant `cargo test -p yoi-e2e --features e2e panel`, `cargo check`, `cargo fmt --check`, `git diff --check`, and `nix build .#yoi` if code/package/runtime behavior changes.
## Non-goals
- Interactive shell の command lookup / prompt rendering まで含めた OS/shell latency の厳密測定。
- すべての background observation が完全 settle するまで UI を出さないこと。
- Panel architecture の全面刷新。
- Ticket lifecycle semantics の変更。
## Related work
- `00001KV62PF32` — Panel startup latency E2E を一覧データ描画完了基準に修正する。単一 fixture row readiness までで不十分だったため request-changes 済み。
- `00001KV5MRH6D` — Panel startup latency E2E / first visible frame separation work。
- `00001KV5D7MG5` — Panel orchestration worktree Ticket state overlay。

View File

@ -0,0 +1,7 @@
<!-- event: create author: "yoi ticket" at: 2026-06-18T13:30:51Z -->
## 作成
LocalTicketBackend によって作成されました。
---

View File

@ -5406,6 +5406,9 @@ fn panel_ticket_detail(row: &PanelRow) -> String {
} }
let mut parts = vec![panel_ticket_reference(row)]; let mut parts = vec![panel_ticket_reference(row)];
if let Some(overlay_detail) = panel_ticket_overlay_detail(row) {
parts.push(overlay_detail);
}
if let Some(blocked_reason) = row if let Some(blocked_reason) = row
.ticket .ticket
.as_ref() .as_ref()
@ -5441,6 +5444,24 @@ fn panel_ticket_action_label(row: &PanelRow, action: NextUserAction) -> &'static
} }
} }
fn panel_ticket_overlay_detail(row: &PanelRow) -> Option<String> {
let ticket = row.ticket.as_ref()?;
let overlay = ticket.orchestration_overlay.as_ref()?;
let mut detail = format!(
"Overlay: local {} · {} {}",
ticket.workflow_state.as_str(),
overlay.source,
overlay.workflow_state.as_str()
);
if matches!(
overlay.workflow_state,
TicketWorkflowState::Done | TicketWorkflowState::Closed
) {
detail.push_str(" · merge pending");
}
Some(detail)
}
fn panel_ticket_reason(row: &PanelRow) -> Option<&str> { fn panel_ticket_reason(row: &PanelRow) -> Option<&str> {
row.disabled_reason row.disabled_reason
.as_deref() .as_deref()
@ -7596,6 +7617,43 @@ branch = "orchestration/custom-panel"
assert!(detail_line.ends_with('…')); assert!(detail_line.ends_with('…'));
} }
#[test]
fn panel_orchestration_overlay_uses_compact_status_column_and_detail_line() {
let mut row = panel_test_ticket_row(
"00001OVERLAY",
"Overlay column regression",
ActionPriority::Background,
NextUserAction::Wait,
"queued",
);
row.kind = PanelRowKind::Review;
row.status = "q→done".to_string();
row.disabled_reason = Some(
"orchestration worktree overlay shows Ticket state done; local state remains queued"
.to_string(),
);
row.ticket.as_mut().unwrap().orchestration_overlay =
Some(crate::workspace_panel::TicketStateOverlay {
source: "orchestration".to_string(),
workflow_state: TicketWorkflowState::Done,
});
let lines = panel_row_lines(&row, false, 160);
let title_line = plain_line(&lines[0]);
let detail_line = plain_line(&lines[1]);
let state_start = 2;
let title_start = state_start + TICKET_STATE_COLUMN_WIDTH + 1;
assert!(row.status.width() <= TICKET_STATE_COLUMN_WIDTH);
assert_eq!(display_column(&title_line, "q→done"), state_start);
assert_eq!(
display_column(&title_line, "Overlay column regression"),
title_start
);
assert!(!title_line.contains("orchestration"));
assert!(detail_line.contains("Overlay: local queued · orchestration done · merge pending"));
}
#[test] #[test]
fn ready_ticket_with_waiting_gate_shows_queue_disabled_reason() { fn ready_ticket_with_waiting_gate_shows_queue_disabled_reason() {
let mut row = panel_test_ticket_row( let mut row = panel_test_ticket_row(

View File

@ -1089,15 +1089,25 @@ fn ticket_state_display(
) -> String { ) -> String {
match overlay { match overlay {
Some(overlay) => format!( Some(overlay) => format!(
"local: {} · {}: {}", "{}→{}",
local.as_str(), compact_ticket_state_label(local),
overlay.source, compact_ticket_state_label(overlay.workflow_state)
overlay.workflow_state.as_str()
), ),
None => local.as_str().to_string(), None => local.as_str().to_string(),
} }
} }
fn compact_ticket_state_label(state: TicketWorkflowState) -> &'static str {
match state {
TicketWorkflowState::Planning => "plan",
TicketWorkflowState::Ready => "ready",
TicketWorkflowState::Queued => "q",
TicketWorkflowState::InProgress => "prog",
TicketWorkflowState::Done => "done",
TicketWorkflowState::Closed => "cls",
}
}
fn apply_orchestration_overlay_to_derived( fn apply_orchestration_overlay_to_derived(
derived: &mut DerivedTicketState, derived: &mut DerivedTicketState,
local: TicketWorkflowState, local: TicketWorkflowState,
@ -1138,51 +1148,90 @@ fn apply_orchestration_overlay_to_derived(
} }
} }
fn format_relation_blockers(blockers: &[&TicketRelationBlocker]) -> String {
let shown_blockers = blockers.iter().take(3).count();
let mut formatted = blockers
.iter()
.take(3)
.map(|blocker| {
format!(
"{} via {} (state: {})",
blocker.blocking_ticket,
blocker.reason_kind,
blocker.blocking_state.as_str()
)
})
.collect::<Vec<_>>()
.join(", ");
let remaining_blockers = blockers.len().saturating_sub(shown_blockers);
if remaining_blockers > 0 {
formatted.push_str(&format!(" (+{remaining_blockers} more)"));
}
formatted
}
fn relation_blocker_allows_ready_queue(blocker: &TicketRelationBlocker) -> bool {
matches!(
blocker.blocking_state,
TicketWorkflowState::Queued | TicketWorkflowState::InProgress
)
}
fn derive_ticket_state( fn derive_ticket_state(
summary: &TicketSummary, summary: &TicketSummary,
relation_blockers: &[TicketRelationBlocker], relation_blockers: &[TicketRelationBlocker],
) -> DerivedTicketState { ) -> DerivedTicketState {
if !relation_blockers.is_empty() { if !relation_blockers.is_empty() {
let shown_blockers = relation_blockers.iter().take(3).count(); let active_blockers = relation_blockers
let mut blockers = relation_blockers
.iter() .iter()
.take(3) .filter(|blocker| !relation_blocker_allows_ready_queue(blocker))
.map(|blocker| { .collect::<Vec<_>>();
format!( if !active_blockers.is_empty() || summary.workflow_state != TicketWorkflowState::Ready {
"{} via {} (state: {})", let blockers_to_report = if active_blockers.is_empty() {
blocker.blocking_ticket, relation_blockers.iter().collect::<Vec<_>>()
blocker.reason_kind, } else {
blocker.blocking_state.as_str() active_blockers
) };
}) let blockers = format_relation_blockers(&blockers_to_report);
.collect::<Vec<_>>() let waiting_reason = format!("waiting for {blockers}");
.join(", "); return DerivedTicketState {
let remaining_blockers = relation_blockers.len().saturating_sub(shown_blockers); kind: match summary.workflow_state {
if remaining_blockers > 0 { TicketWorkflowState::Planning => PanelRowKind::Planning,
blockers.push_str(&format!(" (+{remaining_blockers} more)")); TicketWorkflowState::Queued | TicketWorkflowState::InProgress => {
PanelRowKind::ActiveWork
}
TicketWorkflowState::Done | TicketWorkflowState::Closed => PanelRowKind::Review,
TicketWorkflowState::Ready => PanelRowKind::Ticket,
},
priority: match summary.workflow_state {
TicketWorkflowState::Queued | TicketWorkflowState::InProgress => {
ActionPriority::ActiveWork
}
_ => ActionPriority::Background,
},
action: Some(NextUserAction::Wait),
disabled_reason: Some(format!(
"Queue disabled: {waiting_reason}. Resolve dependency/blocker before ready -> queued."
)),
key_hint: Some(format!("Gate: {waiting_reason}")),
blocked_reason: Some(blockers),
};
} }
let waiting_reason = format!("waiting for {blockers}");
let blockers = format_relation_blockers(
&relation_blockers
.iter()
.collect::<Vec<&TicketRelationBlocker>>(),
);
return DerivedTicketState { return DerivedTicketState {
kind: match summary.workflow_state { kind: PanelRowKind::Ticket,
TicketWorkflowState::Planning => PanelRowKind::Planning, priority: ActionPriority::ReadyForQueue,
TicketWorkflowState::Queued | TicketWorkflowState::InProgress => { action: Some(NextUserAction::Queue),
PanelRowKind::ActiveWork disabled_reason: None,
} key_hint: Some(format!(
TicketWorkflowState::Done | TicketWorkflowState::Closed => PanelRowKind::Review, "Queue allowed: prerequisites are already queued/in progress; Orchestrator will preserve order ({blockers})."
TicketWorkflowState::Ready => PanelRowKind::Ticket,
},
priority: match summary.workflow_state {
TicketWorkflowState::Queued | TicketWorkflowState::InProgress => {
ActionPriority::ActiveWork
}
_ => ActionPriority::Background,
},
action: Some(NextUserAction::Wait),
disabled_reason: Some(format!(
"Queue disabled: {waiting_reason}. Resolve dependency/blocker before ready -> queued."
)), )),
key_hint: Some(format!("Gate: {waiting_reason}")), blocked_reason: None,
blocked_reason: Some(blockers),
}; };
} }
@ -1635,15 +1684,6 @@ mod tests {
.unwrap_or_else(|| panic!("missing row for {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 { fn live_pods(names: &[&str]) -> PodList {
PodList::from_sources( PodList::from_sources(
crate::pod_list::PodVisibilitySource::ResumePicker, crate::pod_list::PodVisibilitySource::ResumePicker,
@ -1731,8 +1771,7 @@ mod tests {
let model = build_workspace_panel(temp.path(), &empty_pods()); let model = build_workspace_panel(temp.path(), &empty_pods());
let matched = ticket_row_by_title(&model, "Overlay Match"); let matched = ticket_row_by_title(&model, "Overlay Match");
status_contains(matched, "local: queued"); assert_eq!(matched.status, "q→prog");
status_contains(matched, "orchestration: inprogress");
assert_eq!( assert_eq!(
matched.ticket.as_ref().unwrap().workflow_state, matched.ticket.as_ref().unwrap().workflow_state,
TicketWorkflowState::Queued TicketWorkflowState::Queued
@ -1772,8 +1811,7 @@ mod tests {
let model = build_workspace_panel(temp.path(), &empty_pods()); let model = build_workspace_panel(temp.path(), &empty_pods());
let row = ticket_row_by_title(&model, "Overlay In Progress"); let row = ticket_row_by_title(&model, "Overlay In Progress");
status_contains(row, "local: queued"); assert_eq!(row.status, "q→prog");
status_contains(row, "orchestration: inprogress");
assert_eq!(row.next_action, Some(NextUserAction::Wait)); assert_eq!(row.next_action, Some(NextUserAction::Wait));
assert_eq!(row.kind, PanelRowKind::ActiveWork); assert_eq!(row.kind, PanelRowKind::ActiveWork);
assert_eq!(fs::read_to_string(&local_item).unwrap(), before); assert_eq!(fs::read_to_string(&local_item).unwrap(), before);
@ -1801,8 +1839,7 @@ mod tests {
let model = build_workspace_panel(temp.path(), &empty_pods()); let model = build_workspace_panel(temp.path(), &empty_pods());
let row = ticket_row_by_title(&model, "Overlay Done"); let row = ticket_row_by_title(&model, "Overlay Done");
status_contains(row, "local: queued"); assert_eq!(row.status, "q→done");
status_contains(row, "orchestration: done");
assert_eq!(row.kind, PanelRowKind::Review); assert_eq!(row.kind, PanelRowKind::Review);
assert_eq!(row.next_action, Some(NextUserAction::Wait)); assert_eq!(row.next_action, Some(NextUserAction::Wait));
assert_ne!(row.next_action, Some(NextUserAction::Queue)); assert_ne!(row.next_action, Some(NextUserAction::Queue));
@ -2123,6 +2160,50 @@ mod tests {
); );
} }
#[test]
fn workspace_panel_allows_ready_ticket_when_relation_prerequisite_is_queued() {
let temp = TempDir::new().unwrap();
write_ticket_config(temp.path());
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
let mut ready_input = NewTicket::new("Ready After Queued Relation");
ready_input.workflow_state = Some(TicketWorkflowState::Ready);
let ready = backend.create(ready_input).unwrap();
let mut dependency_input = NewTicket::new("Queued Relation Dependency");
dependency_input.workflow_state = Some(TicketWorkflowState::Queued);
let dependency = backend.create(dependency_input).unwrap();
backend
.add_ticket_relation(
TicketIdOrSlug::Id(ready.id.clone()),
NewTicketRelation {
kind: TicketRelationKind::DependsOn,
target: dependency.id.clone(),
note: None,
author: Some("test".to_string()),
},
)
.unwrap();
let model = build_workspace_panel(temp.path(), &empty_pods());
let row = model
.rows
.iter()
.find(|row| row.title == "Ready After Queued Relation")
.unwrap();
assert_eq!(row.kind, PanelRowKind::Ticket);
assert_eq!(row.next_action, Some(NextUserAction::Queue));
assert_eq!(row.priority, ActionPriority::ReadyForQueue);
assert!(row.disabled_reason.is_none());
assert!(row.ticket.as_ref().unwrap().blocked_reason.is_none());
assert!(
row.key_hint
.as_deref()
.unwrap()
.contains("Queue allowed: prerequisites are already queued/in progress")
);
assert!(row.key_hint.as_deref().unwrap().contains(&dependency.id));
}
#[test] #[test]
fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() { fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();