test: record panel tui e2e evidence

This commit is contained in:
2026-06-15 01:46:43 +09:00
parent 5e81bc38e6
commit 1f07e57a2c
7 changed files with 135 additions and 9 deletions
+52
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>> {
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()
+10 -4
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 {}",
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(())
+7 -1
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.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,