Skip to content

Workspaces 10/10: harness alert stores through the sidecar - #623

Merged
nedtwigg merged 29 commits into
workspaces-move-verbfrom
workspaces-harness
Sep 15, 2026
Merged

nedtwigg merged 29 commits into
workspaces-move-verbfrom
workspaces-harness

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

The browser harness now sends alert_command and applies the sidecar stores' broadcasts like the Tauri adapter, so the seed-once and delta rules are exercised there. Simulating several windows in the harness is not built.

Based on workspaces-move-verb.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u

The harness applied watched-command and settings changes to its own
AlertManager and ignored the sidecar's broadcasts, so the seed-once and
delta rules the shipped app relies on were never exercised there. It now
sends alert_command like the Tauri adapter and applies the stores'
broadcasts, with the harness forwarding the command to the sidecar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3f8f3a0
Status: ✅  Deploy successful!
Preview URL: https://79da3490.mouseterm.pages.dev
Branch Preview URL: https://workspaces-harness.mouseterm.pages.dev

View logs

@nedtwigg
nedtwigg added this pull request to stack #624 September 10, 2026 21:44

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.

The seed's reply is now the only thing that populates the harness's AlertManager, and the harness can miss it. Before this change alertSetWatchedCommands / alertPublishSettings applied locally, so the manager always had the rules. Now the round trip is the only path: initAlertStateReceiver registers the handlers and immediately POSTs the two seeds, and the canonical snapshot comes back over the SSE stream. But BrowserSidecarHost.init() (standalone/src/browser-sidecar-host.ts, the method that does this.events = new EventSource(url)) resolves as soon as the EventSource is constructed, not when it is open — nothing awaits onopen. If the seed POST reaches the bridge before the /events request registers in sseClients, broadcast() fans out to nobody, the reply is gone, and since WatchedCommandHost.initialize only republishes when something asks again, the harness runs with an empty WATCHING rule set until the user toggles a rule by hand — silently, and only in the dev harness the spec rule you added exists to keep faithful. The same gap reopens on any EventSource auto-reconnect: nothing replays the stores afterwards. Resolving init() on the open event closes the startup half.

Tauri has no equivalent gap — listenToWindow registration is awaited before boot proceeds — so this is the one place where the harness's answer to the two-store rule differs from the shipped one.

The new test covers about half of what its name claims. It asserts the outbound commands and the alert:watchedCommands handler fan-out, but not that the broadcast reaches alertManager (setWatchedCommands / applySettings), and the alert:settings branch — including its if (!settings) return guard — is never delivered. Worth a deliver("alert:settings", …) with an onAlertSettings assertion beside the existing one.

Comment thread standalone/src/browser-sidecar-adapter.test.ts Outdated
nedtwigg and others added 2 commits September 10, 2026 17:32
The seed round trip is now the only thing that populates the harness's
AlertManager, and BrowserSidecarHost.init() resolved as soon as the
EventSource was constructed. If the seed POSTs reached the bridge before
the GET /events registered in sseClients, the broadcast fanned out to
nobody and the harness ran with an empty rule set until a manual toggle.

init() now settles on the stream's open event, rejects on an error
before that (or after a bounded wait) and closes the stream, and reports
every later open through onReconnect. The adapter remembers the two
seeds it last sent and re-offers them on a reconnect, which makes the
sidecar republish both stores: a repeat seed is refused as a seed but
still answered with the canonical snapshot.

The adapter test also asserted a settings field the wire never carries
and never checked that a broadcast reached the manager; it now uses
speakEnabled, spies on setWatchedCommands / applySettings, and delivers
alert:settings including its empty-payload guard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus
Carries every stage's review fixes to the tip. The replay-rebuilds-alerts
test seeds through the sidecar path this stage installs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.

The seed-before-stream gap from the last round is closed, and the replay's premise checks out: WatchedCommandHost.initialize and AlertSettingsHost.initialize both call publish() outside the initialized guard, so a repeat seed really does come back as the canonical snapshot.

init() can now reject, and its only caller can't show that. bootstrap() in standalone/src/main.tsx ends with createRoot(document.getElementById("root")!).render(…) and is invoked as a bare bootstrap(); with no .catch(), so a rejected host.init() aborts before the render: the harness is a blank page whose only explanation is an unhandled rejection in devtools. The old init() never rejected — a pre-open failure logged and EventSource retried on its own until the bridge answered. Either keep that self-healing (inline below) or catch in bootstrap() and put the reason on screen; as it stands the new failure mode is loud in code and silent to whoever ran pnpm innerdogfood.

The reconnect hook covers the alert stores but not the registry. dormouse://workspaces rides the same stream — broadcast('sidecar', { event: 'dormouse://workspaces', … }) in standalone/scripts/dev-agent-browser.mjs — so a drop stales it the same way, and installWorkspaceRegistry already holds the re-fetch it would need (host.invoke<WorkspaceRegistrySnapshot>("workspace_registry")). Inert while the harness is one window, but it is the gap the seed replay just closed, one subscriber away.

Comment thread standalone/src/browser-sidecar-host.ts
Comment thread docs/specs/transport.md Outdated

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.

The retry split lands: EventSource keeps its own reconnect for a CONNECTING failure, OPEN_TIMEOUT_MS still bounds it, and FakeEventSource now carries the readyState/CLOSED pair the assertion needs, so "rejects, and stops the stream, when it errors before opening" is pinning something again.

The new startup-failure UI governs Tauri, but only the harness spec says so. void bootstrap().catch(…) in standalone/src/main.tsx is unconditional — it catches a rejected TauriAdapter.init() (its listenToWindow round trips are awaited and can reject) and a rejected restoreWindowOrFresh, not just the harness's stream timeout. The rule you wrote lives in docs/specs/transport.md → "Standalone browser-dev harness", but bootstrap()'s owning spec is docs/specs/standalone.md → "Boot sequence", which declares Source of truth: standalone/src/main.tsx (bootstrap()) and today carries the neighbouring invariant "planArrival never throws into bootstrap()." A refused sole arrival on the boot path renders a fresh one-pane Workspace, never a blank window — the premise this change generalizes. A reader of standalone.md still sees a boot rejection as unhandled, and AGENTS.md → "House form for rules" puts each rule once in the spec that owns it. Either add the step-9 line there and leave transport.md a pointer, or keep the clause harness-local and gate the catch on BROWSER_DEV_HOST.

RegistryWorkspace.ref's null no longer has a producer. standalone/src/workspace-registry.ts declares ref: string | null under /** `workspace:<n>` for a minted id; `null` for one the registry did not mint. */, but Rust never produced it — ref_for in standalone/src-tauri/src/workspaces.rs is Some(ref_number(id).map_or_else(|| format!("workspace:{id}"), …)), pinned by assert_eq!(ref_for("workspace-abc12345-3"), Some("workspace:workspace-abc12345-3".to_string())). The harness's null was the last one, and this commit removes it. The doc comment is the half that was wrong, so tightening to ref: string with a comment naming the opaque-id fallback makes the type match both hosts; leaving it means every consumer keeps narrowing a case neither host can send.

One housekeeping note on the open thread above: its suggestion block reproduces the pre-commit paragraph, so applying it now would silently drop the retry/startup-error clause you just added. Worth re-composing rather than clicking apply.

Comment thread TESTING_AND_MODIFICATION_GUIDE.md
@nedtwigg
nedtwigg marked this pull request as ready for review September 14, 2026 22:29

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two findings, both on the new guide. The open threads from the draft rounds still stand.

§4's list is not the gate CI runs. Root pnpm test runs public-docs, loopback, deploy (with ps1-cmdlet and installer-verify), and e2e on top of spec-lint, plus every lint's self-test and pnpm -r run test; §4 names only pnpm lint:specs and a bare node scripts/xterm-lint.mjs, which also skips scripts/xterm-bump.test.mjs. An agent who works through §4, sees green, and pushes goes red in CI on a lint the section never named — the outcome the section exists to prevent. Suggestion inline.

§1 and §1a describe state that expires when the stack lands. The table names ten open draft PRs, ten branches, and their worktree paths; "Keep every PR a draft" and the merge-forward procedure hold only while those exist. This file merges to main with the last PR in the stack, and from then on §1 points a new agent at branches and worktrees that are gone while §5 still opens "Nothing below has been run in the Tauri app yet". One line scoping the two sections costs nothing now; suggestion inline.

One judgment call, not a blocker: §7's "Rules that bite" and §6's ratchet recipe paraphrase rules AGENTS.md already owns — "Never git switch -c inside an existing worktree", the --ratchet step, the bare-file-name pointer lint. AGENTS.md -> "What, not why" keeps each rule in the file that owns it and makes every other mention a one-line pointer. This guide is not a spec so no lint enforces that, which is also why the copies will drift silently once AGENTS.md moves.

Comment thread TESTING_AND_MODIFICATION_GUIDE.md
Comment thread TESTING_AND_MODIFICATION_GUIDE.md
@dormouse-bot

Copy link
Copy Markdown
Collaborator

All four findings verified as addressed in #630 — I read the commits rather than taking the claim on trust.

Finding Where Verified
transport.md cited only the host test for a two-part rule 3c5fc72c Both titles now named with their paths: resolves on the stream's open event, not on construction (standalone/src/browser-sidecar-host.test.ts:74) and re-sends its last seeds when the event stream reconnects (standalone/src/browser-sidecar-adapter.test.ts:186). Both exist.
Absolute /Users/ntwigg/... path in the cargo test recipe 3c5fc72c DORMOUSE_MAIN_CHECKOUT with quoted expansions; the "never the worktree's own binaries/ path" warning above it is intact.
§4 read as the whole test gate 8708519b Root pnpm test added as the full CI gate, the lint line relabeled a fast inner loop.
§1/§1a implied permanence 8708519b Scoped to the open stack, with branch/worktree paths marked historical after merge.

One thing worth confirming since it moved rather than changed: 3c5fc72c drops "display fatal startup errors with a reload action" from the transport.md harness paragraph and points at standalone.md → Boot sequence instead. That rule does land there — Must display fatal bootstrap errors with a reload action, scoped to both Tauri and the browser harness — so it is a relocation, not a deletion, and the harness stays covered.

No remaining concerns from my side on this PR.

@nedtwigg
nedtwigg merged commit 43c4a1d into main Sep 15, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants