Skip to content

fix(smoke): anchor mcp-permissions screenshot on TUI-only text#1574

Merged
Aaronontheweb merged 5 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/mcp-permissions-screenshot-anchor
Jul 3, 2026
Merged

fix(smoke): anchor mcp-permissions screenshot on TUI-only text#1574
Aaronontheweb merged 5 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/mcp-permissions-screenshot-anchor

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a flaky screenshot-regression smoke frame: mcp-permissions-server-list in tests/smoke/tapes/screenshots/mcp-permissions.tape intermittently captured the raw shell transcript instead of the rendered netclaw mcp permissions TUI, failing the byte-for-byte baseline comparison.

Found via artifact analysis of the "Screenshot Regression (Linux)" failure on PR #1573 (CI run 28680686756). The failure is independent of that PR — the hazard is latent in the tape on dev and can fire on any sufficiently loaded runner.

Update (run 28682629818): fixing Frame 1 (below) exposed a second, distinct defect on Frame 2 (mcp-permissions-tool-grid) — a VHS asynchronous-screenshot / TUI-teardown race. Root cause + fix in the new section "Frame 2: the deferred-capture race" at the bottom. The two fixes are unrelated bugs that happened to be masked one-behind-the-other.

Diagnosis (Frame 1)

The uploaded mcp-permissions-server-list.actual.png showed the shell with the netclaw mcp permissions command just typed — the TUI had not painted yet. Both Wait+Screen anchors guarding the Frame 1 Screenshot matched text already present in the shell scrollback before the TUI ever launches, so both returned instantly:

  • Wait+Screen@15s /smoke-math/ — pre-matched by the setup output printed by netclaw mcp add: Added MCP server 'smoke-math' (stdio) and Next: run `netclaw mcp permissions` to grant tools and adjust approvals for 'smoke-math'.
  • Wait+Screen@5s /3 tools/ — pre-matched by the tape's own typed readiness loop, still visible on screen: until netclaw mcp list 2>/dev/null | grep -q 'connected (3 tools)'; do sleep 2; done

With both anchors pre-satisfied, the only remaining delay before Screenshot was the documented Sleep 3s settle guard. On a loaded CI runner the TUI took longer than 3s to paint, so the capture fired on the raw shell. The later frame in the same tape (mcp-permissions-tool-grid) passed AE=0 because its anchors (/record-tasks/, /Server default:/) only match post-TUI content.

This violates the authoring conventions in tests/smoke/tapes/README.md (rule 5: anchor every state change on stable substrings from the next view; every Screenshot must be preceded by an anchored Wait+Screen proving we are on the right screen).

Fix (Frame 1)

Replace the two ambiguous Frame 1 anchors with strings only the rendered TUI produces (timeouts preserved):

Before After Why the new anchor is TUI-only
Wait+Screen@15s /smoke-math/ Wait+Screen@15s /MCP Permissions/ Page title from McpToolPermissionsPage.BuildHeader() (new TextNode("MCP Permissions")). Nothing in the setup transcript prints this string; matching it proves the TUI painted.
Wait+Screen@5s /3 tools/ Wait+Screen@5s /Connected, 3 tools/ Exact server-row text from McpToolPermissionsPage.BuildServerList(): $"{s.Name} ({s.Status}, {s.ToolCount} tools)", where Status is the daemon's PascalCase McpConnectionState.Connected.ToString() (via the /api/mcp/statuses DTO). The transcript form emitted by netclaw mcp list is lowercase and comma-less — connected (3 tools) (McpCommand.cs) — so the two can never collide. This also anchors the frame on the settled state the baseline expects (server row rendered as Connected with all 3 tools indexed).

The Sleep 3s settle guard is deliberately kept: it protects against a different hazard (a daemon state refresh repaint immediately after first render — the documented reason it exists) and the committed baseline was captured with it in place.

Frame 2: the deferred-capture race (root cause of run 28682629818)

After the Frame 1 fix went green, CI run 28682629818 (this branch) failed on Frame 2, mcp-permissions-tool-grid. Its actual.png showed the shell transcript (main buffer) with netclaw mcp permissions typed and nothing after it — even though Frame 1 in the same tape run passed AE=0, proving the TUI had painted on the alternate screen. So the terminal was back on the main buffer by the time Frame 2 was captured.

This is not a TUI crash and not the same anchor bug. Evidence, from the collected artifacts of that run:

  1. The tape ran to completion, every attempt. run-screenshots.log shows Wait 30s Screen record-tasks and Wait 10s Screen Server default: both matched (no timeout), vhs exited 0, and the post-tape assertion logged OK — on all 5 attempts. So the tool grid genuinely rendered and the anchors correctly matched TUI-only text.
  2. The GIF is the ground truth. tape-shot-mcp-permissions.gif shows the tool grid (server: smoke-math, Audience, Server enabled, Server default: [Auto], the add/echo/record-tasks rows) rendered and stable for ~17 frames, then a clean transition to the shell transcript at the exact frame Ctrl+Q fires. Pixel-diffing actual.png against the GIF frames: it is far closer to the post-Ctrl+Q transcript frame (AE≈88k) than to the stable tool-grid frames (AE≈131k).
  3. The daemon log is a clean shutdown (cli-stop, exit 0) with no exception/stack trace, and the transcript ends cleanly with no error text — ruling out a crash.
  4. Not a harness/PNG-staleness artifact. run_shot_tape_with_retry clears both PNGs before each retry and re-ran 5×; the transcript reproduced every time — it is deterministic, not stale mixing.

Root cause: VHS's Screenshot does not write the PNG synchronously at the tape line. It hands the request to the render loop, which flushes the current framebuffer on a later tick. Frame 2's Screenshot is the only capture in the tape immediately followed by a screen-destroying key (Ctrl+Q), with no settle between them. Ctrl+Q tears the TUI off the alternate screen buffer and restores the shell transcript on the main buffer before the deferred capture fires — so VHS writes the transcript, not the tool grid. Frame 1's Screenshot is followed by more Wait+Screen steps that keep the same screen up, so its deferred capture is safe — which is exactly why fixing Frame 1 "moved" the failure onto Frame 2's latent race (before the fix, Frame 1 broke the retry loop first).

Mechanically proven with a minimal isolated repro using less (same alt-screen buffer, pinned VHS v0.11.0):

  • Screenshot → immediate q (quit less) → PNG captured the main-buffer scrollback (the bug).
  • ScreenshotSleep 1sq → PNG correctly captured the alt screen (the fix).

This is precisely the hazard tests/smoke/tapes/README.md already documents — "[Screenshot is] bracketed by Sleep 1s … one after so the next keystroke cannot leak into the capture." Frame 2 had the before-sleep but was missing the after-sleep. A prior fix iteration had dropped it.

Fix (Frame 2 + class hardening)

  • mcp-permissions.tape — add Sleep 2s between the Frame 2 Screenshot and Ctrl+Q, holding the tool grid on screen until the deferred capture lands. (The primary fix.)
  • provider-manager.tape, config-search.tape, wizard-screens.tape — these have the identical ScreenshotCtrl+Q teardown pattern with no settle between (wizard-screens even had its Sleep 1s on the wrong side of Ctrl+Q). They pass today only on timing luck. Added a Sleep 1s before their final teardown key to end the whole class rather than let the next runner move the failure again.
  • tests/smoke/tapes/README.md — clarified that the after-sleep must sit between the Screenshot and the next screen-changing key (never after it), with the VHS async-capture reason spelled out.

No product/.cs changes — this is entirely tape/harness authoring correctness.

Audit of sibling frames/tapes (Frame 1 anchors)

Checked every other capture's preceding anchors against what the transcript contains at that point:

  • mcp-permissions Frame 2 (/record-tasks/, /Server default:/): matched only inside the already-painted TUI, and neither string appears in the setup output. OK.
  • config-search.tape: seeded transcript contains backend=duckduckgo (lowercase) and jq JSON; anchors like /Choose the backend Netclaw uses for web search/, /DuckDuckGo works without setup/, /Brave Search requires an API key/, /\[Enter\] Settings Areas/ never appear in it. OK.
  • provider-manager.tape (/Ollama/): no setup commands precede the launch; the preamble exports contain no "Ollama". OK.
  • wizard-screens.tape (/Choose your LLM provider:/, /7\. Venice\.ai/, /Who will interact with this Netclaw instance/): no transcript collision. OK.
  • help.tape (/Usage:/): the shell output is the intended capture. OK.

Validation

  • scripts/smoke/run-smoke.sh screenshots3 consecutive local runs, all green (VHS v0.11.0, the CI-pinned version; reusing this run's CI-built linux-x64 binaries): all 9 frames matched baselines each run. mcp-permissions-tool-grid now passes at AE=0 on the first attempt with zero retries (previously it retried 5× and then failed the comparison); mcp-permissions-server-list at AE=0 every run.
  • Targeted single-tape stress: 8/8 isolated mcp-permissions runs green, mcp-permissions-tool-grid AE=0 every time — directly exercising the Frame 2 race in isolation.
  • Minimal less alt-screen repro confirms the VHS deferred-capture mechanism and that a settle sleep between Screenshot and the teardown key eliminates it.
  • pwsh -File scripts/Add-FileHeaders.ps1 -Verify — all files have headers. No .cs changed, so dotnet slopwatch analyze has no new surface (tape/doc-only diff).

The mcp-permissions-server-list frame intermittently captured the raw
shell transcript instead of the rendered `netclaw mcp permissions` TUI
(CI run 28680686756, job "Screenshot Regression (Linux)" on PR netclaw-dev#1573).

Root cause: both Wait+Screen anchors preceding the Frame 1 Screenshot
matched text that is already sitting in the shell scrollback BEFORE the
TUI paints, so both waits returned instantly:

- `Wait+Screen@15s /smoke-math/` matched the setup output "Added MCP
  server 'smoke-math' (stdio)" and "...adjust approvals for
  'smoke-math'." printed by `netclaw mcp add`.
- `Wait+Screen@5s /3 tools/` matched the tape's OWN typed readiness
  loop, still visible on screen: `until netclaw mcp list 2>/dev/null |
  grep -q 'connected (3 tools)'; do sleep 2; done`.

With both anchors pre-satisfied, the only remaining delay was the
documented `Sleep 3s` settle guard. On a loaded CI runner the TUI took
longer than 3s to paint, so Screenshot fired while the screen still
showed the shell transcript, failing the byte-for-byte baseline
comparison. This violates the tapes/README.md rule to anchor every
step on stable substrings from the NEXT view.

Fix: anchor Frame 1 on strings that only the rendered TUI produces:

- `MCP Permissions` — the page title from
  McpToolPermissionsPage.BuildHeader; proves the alternate screen
  buffer painted. Nothing in the setup transcript prints this string.
- `Connected, 3 tools` — the exact server-row text from
  McpToolPermissionsPage.BuildServerList, rendered as
  "{Name}  ({Status}, {ToolCount} tools)". The daemon reports the
  state as PascalCase "Connected" (McpConnectionState.ToString());
  the CLI transcript form is lowercase and comma-less
  ("connected (3 tools)"), so the two can never collide.

The `Sleep 3s` settle guard is kept: it protects against a different
hazard (daemon state refresh immediately after first render) and the
committed baseline was captured with it in place.

The tool-grid frame's anchors (/record-tasks/, /Server default:/) and
the other screenshot tapes were audited for the same hazard: all of
their pre-capture anchors match only post-TUI content, so no other
changes were needed.

Validation: `scripts/smoke/run-smoke.sh screenshots` passed 3
consecutive runs locally, all 9 frames matching baselines
(mcp-permissions-server-list at AE=0 each run).
@Aaronontheweb Aaronontheweb added bug Something isn't working tests All issues related to testing, quality assurance, and smoke testing. labels Jul 3, 2026
@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) July 3, 2026 20:28
…ferred-capture race)

The Frame-1 anchor fix on this branch exposed a second, distinct defect on
Frame 2 (mcp-permissions-tool-grid) in CI run 28682629818: the capture showed
the restored shell transcript even though the tool grid had rendered and its
Wait+Screen anchors matched.

Root cause: VHS Screenshot is not synchronous — it flushes the current
framebuffer on a later render tick. Frame 2's Screenshot is the only capture in
the tape immediately followed by a screen-destroying key (Ctrl+Q); the TUI
teardown restores the main buffer before the deferred capture lands, so VHS
writes the transcript instead of the tool grid. Proven with a minimal `less`
alt-screen repro (Screenshot->immediate-q captures the main buffer;
Screenshot->Sleep->q captures the alt screen). This is exactly the hazard the
tapes README already documents ("one after so the next keystroke cannot leak
into the capture") — Frame 2 had the before-sleep but was missing the after.

- mcp-permissions.tape: add Sleep 2s between the Frame 2 Screenshot and Ctrl+Q.
- provider-manager / config-search / wizard-screens: same latent
  Screenshot->Ctrl+Q pattern with no settle; add Sleep 1s before the teardown
  key to end the class (wizard-screens had its Sleep on the wrong side of Ctrl+Q).
- README: clarify the after-sleep must sit between Screenshot and the next
  screen-changing key, with the VHS async-capture rationale.

Validation: 3 consecutive green `run-smoke.sh screenshots` runs (VHS v0.11.0);
mcp-permissions-tool-grid now AE=0 on the first attempt with zero retries.
Tape/doc-only change; no .cs touched.
@Aaronontheweb
Aaronontheweb merged commit 667ee68 into netclaw-dev:dev Jul 3, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working tests All issues related to testing, quality assurance, and smoke testing.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant