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.
This commit is contained in:
Levi Neuwirth 2026-07-28 15:28:14 -04:00
parent 80b761bb03
commit 9c79ce13b2
2 changed files with 31 additions and 2 deletions

View File

@ -779,11 +779,20 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 {
.ok() .ok()
.and_then(|value| value.parse::<u64>().ok()) .and_then(|value| value.parse::<u64>().ok())
.map(std::time::Duration::from_millis); .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 quiet = observe_window.is_some();
let deadline = std::time::Instant::now() let deadline = std::time::Instant::now()
+ observe_window.unwrap_or_else(|| std::time::Duration::from_secs(20)); + observe_window.unwrap_or_else(|| std::time::Duration::from_secs(20));
let mut sent_input = false; let mut sent_input = false;
let mut sent_resize = false; let mut sent_resize = false;
let mut completion_observed = false;
while std::time::Instant::now() < deadline { while std::time::Instant::now() < deadline {
let Ok(event) = rx.recv_timeout(std::time::Duration::from_millis(200)) else { let Ok(event) = rx.recv_timeout(std::time::Duration::from_millis(200)) else {
continue; continue;
@ -857,15 +866,20 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 {
facts.observed_resized_frame = true; 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 // 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 // produces a self-contradictory "successful" probe report
// whose later acceptance assertion must reject it. // whose later acceptance assertion must reject it.
if !quiet if !quiet
&& facts.observed_resized_frame && facts.observed_resized_frame
&& facts.rendered_nonuniform_frames >= 2 && facts.rendered_nonuniform_frames >= 2
&& facts.last_frame_text.contains("VTERMROW") && fixture_evidence_observed
{ {
completion_observed = true;
break; 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_title={}", facts.last_title.unwrap_or_default());
let _ = writeln!(out, "last_frame_text={}", facts.last_frame_text); let _ = writeln!(out, "last_frame_text={}", facts.last_frame_text);
let _ = writeln!(out, "input_echo_observed={}", facts.input_echo_observed); 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()); let _ = writeln!(out, "disconnect={}", facts.disconnect.unwrap_or_default());
if let Err(error) = std::fs::write(report, out) { if let Err(error) = std::fs::write(report, out) {
eprintln!( eprintln!(

View File

@ -691,6 +691,9 @@ fn a37_real_daemon_real_pty_and_headless_gpu_render_one_terminal_session() {
.arg(&report) .arg(&report)
// The chord the probe presses to run `vterm-probe.open`. // The chord the probe presses to run `vterm-probe.open`.
.env("PMACS_GPU_PROBE_OPEN_KEY", "t") .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() .output()
.expect("run the headless GPU probe"); .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")), .is_some_and(|t| t.contains("VTERMROW")),
"the child's cursor-addressed output must reach the rendered frame: {text}" "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 let declarations: u32 = facts
.get("declarations") .get("declarations")
.and_then(|v| v.parse().ok()) .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: {}", "the typed character must reach the child and return: {}",
report() 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()
);
} }