Merge branch 'claude/distracted-ritchie-53fc70'

Bounds the renderer wait, so a WebView2 that never initialises reports itself
instead of hanging Connect with the busy flag stuck.

Conflict resolution, all of it in the App test suite, which main had changed
under the branch when sleepy-chebyshev landed:

- The workspace fixture keeps main's fake SSH factory and its FakeRenderer-aware
  page, and takes the branch's RendererTimeout on top. One second rather than
  the branch's 250 ms, because the timeout now also bounds FakeRenderer's own
  wait for the attach it just made.
- FakeRenderer arrived on main after the branch was cut and still called the
  no-argument WaitForRendererAsync. Both sides merged cleanly and left the build
  broken; it now passes its own token.
- ConnectingWithNoRenderer's remark claimed the suite never starts the workspace
  and never attaches a renderer. Both are false here, so it now says what is
  true of the test: it is the one connect test that attaches no renderer.
This commit is contained in:
2026-07-29 15:40:25 +02:00
7 changed files with 175 additions and 14 deletions
+13 -4
View File
@@ -90,10 +90,19 @@ the case it was attached to. A process-level check cannot verify a rendering cla
screenshot, and this defect shipped because one was never taken.
**What the first connection after unlocking actually depends on** is the `await
workspace.WaitForRendererAsync()` in `VaultViewModel.ConnectAsync`, because `TerminalDataPlane.SendAsync`
drops frames when no renderer is attached rather than queueing them. That await is the invariant; the
control's visibility is not. It currently has no timeout, so a WebView2 that fails to initialise hangs
Connect with the busy flag stuck — worth fixing on its own merits.
workspace.WaitForRendererAsync(cancellationToken)` in `VaultViewModel.ConnectAsync`, because
`TerminalDataPlane.SendAsync` drops frames when no renderer is attached rather than queueing them. That
await is the invariant; the control's visibility is not.
It is now bounded — `TerminalWorkspaceOptions.RendererTimeout`, 15 s, plus the command's own token —
because whether the renderer attaches at all depends on a runtime this application does not install. A
missing or policy-blocked Evergreen runtime, or an AppContainer that cannot reach loopback, previously
left Connect waiting forever with `IsBusy` stuck and nothing on screen to explain it. The gate is
unchanged; only the wait is. Why 15 s and not less: attaching is near-instant in the normal case (the page
attaches while the unlock screen is still up), but a cold WebView2 profile creates a user-data directory
and starts its process tree first, and reporting a broken runtime to someone whose runtime was merely slow
is the worse error. The timeout is caught in `VaultViewModel` and reported as a message naming WebView2,
because `TimeoutException.Message` is "The operation has timed out" and names nothing.
**Hiding the WebView does not pause it.** With the holder window hidden, the page keeps
`visibilityState: "visible"` and `requestAnimationFrame` keeps firing at roughly 115/s — Chromium does not