From 9c79ce13b2b73f148c15e9110c41bc044dfa1068 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 28 Jul 2026 15:28:14 -0400 Subject: [PATCH] Fix fixture-specific GPU probe completion Let producer probes name the frame text they require while input probes finish on their latched echo observation. Report and assert whether the probe reached that evidence so the 20-second safety deadline cannot masquerade as successful completion. The CAT acceptance now finishes in 0.32 seconds instead of waiting out the full deadline, while the VTERMROW producer still waits for its own PTY breadcrumb. --- pmacs-gpu/src/main.rs | 19 +++++++++++++++++-- tests/vterm_stage3_acceptance.rs | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index a35de2d..3a8bb55 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -779,11 +779,20 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 { .ok() .and_then(|value| value.parse::().ok()) .map(std::time::Duration::from_millis); + // Normal probes stop only after their fixture-specific evidence arrives. + // A producer fixture names the text it must paint; an input fixture uses + // the latched echo observation. Keeping that choice outside this generic + // runner prevents one fixture's breadcrumb from forcing another fixture + // to sit on the 20-second safety deadline. + let expected_frame_text = std::env::var("PMACS_GPU_PROBE_EXPECT_TEXT") + .ok() + .filter(|value| !value.is_empty()); let quiet = observe_window.is_some(); let deadline = std::time::Instant::now() + observe_window.unwrap_or_else(|| std::time::Duration::from_secs(20)); let mut sent_input = false; let mut sent_resize = false; + let mut completion_observed = false; while std::time::Instant::now() < deadline { let Ok(event) = rx.recv_timeout(std::time::Duration::from_millis(200)) else { continue; @@ -857,15 +866,20 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 { facts.observed_resized_frame = true; } } + let fixture_evidence_observed = expected_frame_text.as_deref().map_or_else( + || facts.input_echo_observed, + |expected| facts.last_frame_text.contains(expected), + ); // Do not exit merely because resize/composition happened - // first: that races the PTY child's initial output and + // first: that races the fixture's required PTY evidence and // produces a self-contradictory "successful" probe report // whose later acceptance assertion must reject it. if !quiet && facts.observed_resized_frame && facts.rendered_nonuniform_frames >= 2 - && facts.last_frame_text.contains("VTERMROW") + && fixture_evidence_observed { + completion_observed = true; break; } } @@ -898,6 +912,7 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 { let _ = writeln!(out, "last_title={}", facts.last_title.unwrap_or_default()); let _ = writeln!(out, "last_frame_text={}", facts.last_frame_text); let _ = writeln!(out, "input_echo_observed={}", facts.input_echo_observed); + let _ = writeln!(out, "completion_observed={completion_observed}"); let _ = writeln!(out, "disconnect={}", facts.disconnect.unwrap_or_default()); if let Err(error) = std::fs::write(report, out) { eprintln!( diff --git a/tests/vterm_stage3_acceptance.rs b/tests/vterm_stage3_acceptance.rs index 9656491..f6b1f7f 100644 --- a/tests/vterm_stage3_acceptance.rs +++ b/tests/vterm_stage3_acceptance.rs @@ -691,6 +691,9 @@ fn a37_real_daemon_real_pty_and_headless_gpu_render_one_terminal_session() { .arg(&report) // The chord the probe presses to run `vterm-probe.open`. .env("PMACS_GPU_PROBE_OPEN_KEY", "t") + // This producer fixture does not consume the probe's input; wait + // instead for its own live cursor-addressed breadcrumb. + .env("PMACS_GPU_PROBE_EXPECT_TEXT", "VTERMROW") .output() .expect("run the headless GPU probe"); @@ -746,6 +749,11 @@ fn a37_real_daemon_real_pty_and_headless_gpu_render_one_terminal_session() { .is_some_and(|t| t.contains("VTERMROW")), "the child's cursor-addressed output must reach the rendered frame: {text}" ); + assert_eq!( + facts.get("completion_observed").copied(), + Some("true"), + "the probe must finish on the fixture's PTY evidence, not its deadline: {text}" + ); let declarations: u32 = facts .get("declarations") .and_then(|v| v.parse().ok()) @@ -1311,4 +1319,10 @@ fn gpu_terminal_input_reaches_the_child_and_returns_in_a_frame() { "the typed character must reach the child and return: {}", report() ); + assert_eq!( + facts.get("completion_observed").map(String::as_str), + Some("true"), + "the probe must finish on the latched input echo, not its deadline: {}", + report() + ); }