diff --git a/.yoi/tickets/00001KV3BQ7Q3/artifacts/e2e-evidence.md b/.yoi/tickets/00001KV3BQ7Q3/artifacts/e2e-evidence.md new file mode 100644 index 00000000..74cea840 --- /dev/null +++ b/.yoi/tickets/00001KV3BQ7Q3/artifacts/e2e-evidence.md @@ -0,0 +1,27 @@ +# E2E evidence for Ticket 00001KV3BQ7Q3 + +Validation date: 2026-06-14 +Worktree: `/home/hare/Projects/yoi/.worktree/00001KV3BQ7Q3-panel-e2e-evidence` +Branch: `impl/00001KV3BQ7Q3-panel-e2e-evidence` + +## Summary + +| Target merge behavior | Status | E2E scenario / assertion | +| --- | --- | --- | +| `802fa1f00f8725fe35336e083cd05652fee1409e` / `merge: rewind live refresh` | Pass for current fixture PTY E2E | `single_pod_rewind_picker_applies_without_escape_and_suppresses_duplicate_enter` spawns the real `yoi` binary under PTY, opens the rewind picker, applies the target without Esc/restart/restore, observes `rewind_applied` with restored composer text, and now also waits for the post-apply PTY stream to contain the unique live composer marker `rewind-live-refresh`. | +| `02311883f7cda116676d8e179a14ad0be9e7a244` / `merge: panel mouse selection` | Pass for current fixture PTY E2E | `panel_mouse_click_selects_row_without_dispatching_action` spawns the real `yoi panel` PTY path, injects SGR mouse click input, observes `selection_changed` for the clicked row, and asserts no `action_requested` event was emitted by click alone. | +| `db7bad7a64766c2039a4c10781801cb571027955` / `merge: panel quit latency` | Pass for bounded current fixture PTY E2E; original live-terminal latency remains outside this fixture | `panel_ctrl_c_exits_promptly_after_background_barrier` spawns the real `yoi panel` PTY path with a held `reload` background task, confirms that task is pending, sends Ctrl-C, and asserts clean process exit within `PanelHarness::default_exit_wait()` (1500 ms) plus `quit_requested` and `background_task_aborted { task: "reload" }` events. This guarantees that pending fixture background reload work is aborted and does not block quit past the threshold; it does not prove arbitrary live-terminal latency outside this fixture. | + +## Commands and results + +- `cargo fmt --check` — passed. +- `cargo test -p yoi-e2e --features e2e --no-run` — passed; built `yoi-e2e` unit/integration test executables. +- `cargo test -p yoi-e2e --features e2e` — passed: `yoi_e2e` unit test 1/1, `panel` integration tests 3/3, `rewind` integration test 1/1, doc-tests 0. +- `cargo check -p yoi-e2e -p yoi -p tui` — passed. +- `git diff --check` — passed. + +## Residual gaps / non-claims + +- These are automated fixture PTY confirmations only. They are not manual/live-terminal validation. +- The mouse path uses the harness's SGR mouse injection through a PTY. It confirms the real `yoi panel` process path receives and handles the encoded click as intended, but it is not a hardware/terminal-emulator compatibility matrix. +- The quit-latency assertion is bounded to the fixture's held `reload` task and 1500 ms threshold. It confirms pending fixture background work does not user-visibly block quit beyond that bound, but does not independently reproduce every historical live latency observation. diff --git a/.yoi/tickets/00001KV3BQ7Q3/item.md b/.yoi/tickets/00001KV3BQ7Q3/item.md index ef82c54a..b4dfd5da 100644 --- a/.yoi/tickets/00001KV3BQ7Q3/item.md +++ b/.yoi/tickets/00001KV3BQ7Q3/item.md @@ -2,7 +2,7 @@ title: '対象 TUI/Panel merge commit の挙動を現行 E2E で確認する' state: 'inprogress' created_at: '2026-06-14T15:24:05Z' -updated_at: '2026-06-14T16:39:19Z' +updated_at: '2026-06-14T16:45:57Z' assignee: null readiness: 'implementation_ready' risk_flags: ['e2e', 'tui', 'panel', 'regression-evidence'] diff --git a/.yoi/tickets/00001KV3BQ7Q3/thread.md b/.yoi/tickets/00001KV3BQ7Q3/thread.md index 84fb8047..44654a26 100644 --- a/.yoi/tickets/00001KV3BQ7Q3/thread.md +++ b/.yoi/tickets/00001KV3BQ7Q3/thread.md @@ -113,4 +113,39 @@ Critical risks / reviewer focus: Routing decision と accepted implementation/evidence plan を記録済み。先行 Panel/TUI implementation Tickets は merge/validation/done 済みで、prior waiting reason は解消。blocking relation / unresolved orchestration-plan blocker はないため、E2E validation side effects の前に `queued -> inprogress` acceptance を記録する。 +--- + + + +## Implementation report + +# E2E evidence for Ticket 00001KV3BQ7Q3 + +Validation date: 2026-06-14 +Worktree: `/home/hare/Projects/yoi/.worktree/00001KV3BQ7Q3-panel-e2e-evidence` +Branch: `impl/00001KV3BQ7Q3-panel-e2e-evidence` + +## Summary + +| Target merge behavior | Status | E2E scenario / assertion | +| --- | --- | --- | +| `802fa1f00f8725fe35336e083cd05652fee1409e` / `merge: rewind live refresh` | Pass for current fixture PTY E2E | `single_pod_rewind_picker_applies_without_escape_and_suppresses_duplicate_enter` spawns the real `yoi` binary under PTY, opens the rewind picker, applies the target without Esc/restart/restore, observes `rewind_applied` with restored composer text, and now also waits for the post-apply PTY stream to contain the unique live composer marker `rewind-live-refresh`. | +| `02311883f7cda116676d8e179a14ad0be9e7a244` / `merge: panel mouse selection` | Pass for current fixture PTY E2E | `panel_mouse_click_selects_row_without_dispatching_action` spawns the real `yoi panel` PTY path, injects SGR mouse click input, observes `selection_changed` for the clicked row, and asserts no `action_requested` event was emitted by click alone. | +| `db7bad7a64766c2039a4c10781801cb571027955` / `merge: panel quit latency` | Pass for bounded current fixture PTY E2E; original live-terminal latency remains outside this fixture | `panel_ctrl_c_exits_promptly_after_background_barrier` spawns the real `yoi panel` PTY path with a held `reload` background task, confirms that task is pending, sends Ctrl-C, and asserts clean process exit within `PanelHarness::default_exit_wait()` (1500 ms) plus `quit_requested` and `background_task_aborted { task: "reload" }` events. This guarantees that pending fixture background reload work is aborted and does not block quit past the threshold; it does not prove arbitrary live-terminal latency outside this fixture. | + +## Commands and results + +- `cargo fmt --check` — passed. +- `cargo test -p yoi-e2e --features e2e --no-run` — passed; built `yoi-e2e` unit/integration test executables. +- `cargo test -p yoi-e2e --features e2e` — passed: `yoi_e2e` unit test 1/1, `panel` integration tests 3/3, `rewind` integration test 1/1, doc-tests 0. +- `cargo check -p yoi-e2e -p yoi -p tui` — passed. +- `git diff --check` — passed. + +## Residual gaps / non-claims + +- These are automated fixture PTY confirmations only. They are not manual/live-terminal validation. +- The mouse path uses the harness's SGR mouse injection through a PTY. It confirms the real `yoi panel` process path receives and handles the encoded click as intended, but it is not a hardware/terminal-emulator compatibility matrix. +- The quit-latency assertion is bounded to the fixture's held `reload` task and 1500 ms threshold. It confirms pending fixture background work does not user-visibly block quit beyond that bound, but does not independently reproduce every historical live latency observation. + + --- diff --git a/crates/tui/src/single_pod.rs b/crates/tui/src/single_pod.rs index 07e1b63b..8a423538 100644 --- a/crates/tui/src/single_pod.rs +++ b/crates/tui/src/single_pod.rs @@ -489,7 +489,7 @@ async fn run_e2e_rewind_fixture( truncate_entries: 1, turn_index: 1, timestamp_ms: Some(1), - preview: "revise the plan".to_string(), + preview: "candidate rewind target".to_string(), eligible: true, disabled_reason: None, warning: None, @@ -500,7 +500,7 @@ async fn run_e2e_rewind_fixture( "rewind_picker_opened", serde_json::json!({ "targets": 1, - "selected_preview": "revise the plan", + "selected_preview": "candidate rewind target", }), ); } @@ -545,7 +545,7 @@ async fn run_e2e_rewind_fixture( if submitted_at.elapsed() >= apply_delay { app.handle_pod_event(Event::RewindApplied { entries: Vec::new(), - input: vec![Segment::text("revise the plan")], + input: vec![Segment::text("rewind-live-refresh")], summary: RewindSummary { truncated_to_entries: 1, discarded_entries: 2, diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index b3dc4c35..9d469df9 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -638,6 +638,43 @@ impl PanelHarness { } } + pub fn output_len(&self) -> usize { + self.output.lock().map(|output| output.len()).unwrap_or(0) + } + + pub fn wait_for_output_contains_from( + &mut self, + start_offset: usize, + needle: &str, + timeout: Duration, + ) -> Result<()> { + let start = Instant::now(); + let needle = needle.as_bytes(); + loop { + if self.output_after(start_offset, needle) { + return Ok(()); + } + if let Some(status) = self.child.try_wait()? { + self.flush_output_artifact()?; + return Err(HarnessError::Protocol(format!( + "process exited with {status} before PTY output contained {:?}", + String::from_utf8_lossy(needle) + ))); + } + if start.elapsed() >= timeout { + self.flush_output_artifact()?; + return Err(HarnessError::Timeout { + what: format!( + "PTY output containing {:?} after offset {start_offset}", + String::from_utf8_lossy(needle) + ), + artifacts: self.artifacts.clone(), + }); + } + thread::sleep(Duration::from_millis(20)); + } + } + pub fn events(&mut self) -> Result> { let text = fs::read_to_string(&self.artifacts.events_jsonl)?; text.lines() @@ -684,6 +721,21 @@ impl PanelHarness { Ok(()) } + fn output_after(&self, start_offset: usize, needle: &[u8]) -> bool { + if needle.is_empty() { + return true; + } + self.output + .lock() + .map(|output| { + let start = start_offset.min(output.len()); + output[start..] + .windows(needle.len()) + .any(|window| window == needle) + }) + .unwrap_or(false) + } + fn mouse_capture_enabled(&self) -> bool { self.output .lock() diff --git a/tests/e2e/tests/panel.rs b/tests/e2e/tests/panel.rs index 5b094758..7a15208a 100644 --- a/tests/e2e/tests/panel.rs +++ b/tests/e2e/tests/panel.rs @@ -135,14 +135,20 @@ fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> "quit latency {elapsed:?} exceeded threshold; artifacts at {}", panel.artifacts().dir.display() ); + let events = panel.events()?; assert!( - panel - .events()? - .iter() - .any(|event| event.event == "quit_requested"), + events.iter().any(|event| event.event == "quit_requested"), "quit_requested observability event missing; artifacts at {}", panel.artifacts().dir.display() ); + assert!( + events.iter().any(|event| { + event.event == "background_task_aborted" + && event.data.get("task").and_then(serde_json::Value::as_str) == Some("reload") + }), + "pending reload task should be aborted before quit completes; artifacts at {}", + panel.artifacts().dir.display() + ); drop(panel); assert_fixture_cleanup(fixture.cleanup()?); Ok(()) diff --git a/tests/e2e/tests/rewind.rs b/tests/e2e/tests/rewind.rs index 386bde93..a2c9f750 100644 --- a/tests/e2e/tests/rewind.rs +++ b/tests/e2e/tests/rewind.rs @@ -13,6 +13,7 @@ fn single_pod_rewind_picker_applies_without_escape_and_suppresses_duplicate_ente tui.assert_no_full_drag_mouse_capture()?; tui.expect_event("rewind_fixture_ready", Duration::from_secs(5))?; + let before_rewind_output = tui.output_len(); tui.press(KeyPress::CtrlR)?; tui.expect_event("rewind_picker_opened", Duration::from_secs(5))?; @@ -34,10 +35,15 @@ fn single_pod_rewind_picker_applies_without_escape_and_suppresses_duplicate_ente .data .get("composer_text") .and_then(serde_json::Value::as_str), - Some("revise the plan"), + Some("rewind-live-refresh"), "rewind should update the visible composer state without Esc/restart; artifacts at {}", tui.artifacts().dir.display() ); + tui.wait_for_output_contains_from( + before_rewind_output, + "rewind-live-refresh", + Duration::from_secs(5), + )?; assert_eq!( tui.count_events("rewind_submit_sent")?, submit_count,