test: record panel tui e2e evidence

This commit is contained in:
Keisuke Hirata 2026-06-15 01:46:43 +09:00
parent 5e81bc38e6
commit 1f07e57a2c
No known key found for this signature in database
7 changed files with 135 additions and 9 deletions

View File

@ -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.

View File

@ -2,7 +2,7 @@
title: '対象 TUI/Panel merge commit の挙動を現行 E2E で確認する' title: '対象 TUI/Panel merge commit の挙動を現行 E2E で確認する'
state: 'inprogress' state: 'inprogress'
created_at: '2026-06-14T15:24:05Z' created_at: '2026-06-14T15:24:05Z'
updated_at: '2026-06-14T16:39:19Z' updated_at: '2026-06-14T16:45:57Z'
assignee: null assignee: null
readiness: 'implementation_ready' readiness: 'implementation_ready'
risk_flags: ['e2e', 'tui', 'panel', 'regression-evidence'] risk_flags: ['e2e', 'tui', 'panel', 'regression-evidence']

View File

@ -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 を記録する。 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 を記録する。
---
<!-- event: implementation_report author: hare at: 2026-06-14T16:45:57Z -->
## 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.
--- ---

View File

@ -489,7 +489,7 @@ async fn run_e2e_rewind_fixture(
truncate_entries: 1, truncate_entries: 1,
turn_index: 1, turn_index: 1,
timestamp_ms: Some(1), timestamp_ms: Some(1),
preview: "revise the plan".to_string(), preview: "candidate rewind target".to_string(),
eligible: true, eligible: true,
disabled_reason: None, disabled_reason: None,
warning: None, warning: None,
@ -500,7 +500,7 @@ async fn run_e2e_rewind_fixture(
"rewind_picker_opened", "rewind_picker_opened",
serde_json::json!({ serde_json::json!({
"targets": 1, "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 { if submitted_at.elapsed() >= apply_delay {
app.handle_pod_event(Event::RewindApplied { app.handle_pod_event(Event::RewindApplied {
entries: Vec::new(), entries: Vec::new(),
input: vec![Segment::text("revise the plan")], input: vec![Segment::text("rewind-live-refresh")],
summary: RewindSummary { summary: RewindSummary {
truncated_to_entries: 1, truncated_to_entries: 1,
discarded_entries: 2, discarded_entries: 2,

View File

@ -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<Vec<HarnessEvent>> { pub fn events(&mut self) -> Result<Vec<HarnessEvent>> {
let text = fs::read_to_string(&self.artifacts.events_jsonl)?; let text = fs::read_to_string(&self.artifacts.events_jsonl)?;
text.lines() text.lines()
@ -684,6 +721,21 @@ impl PanelHarness {
Ok(()) 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 { fn mouse_capture_enabled(&self) -> bool {
self.output self.output
.lock() .lock()

View File

@ -135,14 +135,20 @@ fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()>
"quit latency {elapsed:?} exceeded threshold; artifacts at {}", "quit latency {elapsed:?} exceeded threshold; artifacts at {}",
panel.artifacts().dir.display() panel.artifacts().dir.display()
); );
let events = panel.events()?;
assert!( 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 {}", "quit_requested observability event missing; artifacts at {}",
panel.artifacts().dir.display() 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); drop(panel);
assert_fixture_cleanup(fixture.cleanup()?); assert_fixture_cleanup(fixture.cleanup()?);
Ok(()) Ok(())

View File

@ -13,6 +13,7 @@ fn single_pod_rewind_picker_applies_without_escape_and_suppresses_duplicate_ente
tui.assert_no_full_drag_mouse_capture()?; tui.assert_no_full_drag_mouse_capture()?;
tui.expect_event("rewind_fixture_ready", Duration::from_secs(5))?; tui.expect_event("rewind_fixture_ready", Duration::from_secs(5))?;
let before_rewind_output = tui.output_len();
tui.press(KeyPress::CtrlR)?; tui.press(KeyPress::CtrlR)?;
tui.expect_event("rewind_picker_opened", Duration::from_secs(5))?; 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 .data
.get("composer_text") .get("composer_text")
.and_then(serde_json::Value::as_str), .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 {}", "rewind should update the visible composer state without Esc/restart; artifacts at {}",
tui.artifacts().dir.display() tui.artifacts().dir.display()
); );
tui.wait_for_output_contains_from(
before_rewind_output,
"rewind-live-refresh",
Duration::from_secs(5),
)?;
assert_eq!( assert_eq!(
tui.count_events("rewind_submit_sent")?, tui.count_events("rewind_submit_sent")?,
submit_count, submit_count,