test(ws): add a harness for the WS controller macro paths - #182
Conversation
The WS controller has no test coverage — `activation.test.ts` and `macros.test.ts` both mock `../ws/controller.js` out entirely — so there is nowhere to assert what a macro action sends to Strom or broadcasts to subscribers. Adds that harness: `handleMessage` driven directly with CouchDB and StromClient mocked, asserting the Strom call sequence and the broadcast payloads. State is established through real inbound messages rather than by reaching into the module-level maps, so each case exercises the same state machine the server runs in production. One case here, a macro CUT with no PiP anywhere, which pins the behaviour the PiP work must leave untouched. Cases that require a behaviour change to pass belong with the commits that make them pass. `handleMessage` is exported for this and is the only production change; the plugin exposes just a websocket route, so there was no other seam. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
0bcbc0b to
0bcfe3d
Compare
birme
left a comment
There was a problem hiding this comment.
Verified by actually running it, not just reading it: cloned the branch, merged onto current main (clean, no conflicts), npx tsc --noEmit clean, npx vitest run — 106/106 passing across 10 files (up from 105/105 across 9 on main), including the new harness's 1 test. Note: the PR description's '27 pre-existing failures in activation.test.ts/macros.test.ts, present on main too' is now stale — current main is fully green (105/105), presumably fixed by other work merged since this branch was cut on Aug 27; not a defect in this PR.
Production diff is exactly what the description says: one line, exporting the already-private handleMessage for the test to call directly — no logic change. clearPipState and setTally, which the new test imports, both already exist and are already exported on main (not fabricated). The harness itself is well-built: it points the real StromClient at a throwaway HTTP server rather than a hand-rolled mock, so a signature drift in StromClient would actually be caught (an untyped vi.mock stand-in wouldn't). PiP state is set up via real inbound messages (SELECT_PVW_PIP, TAKE) rather than reaching into module internals, so the test exercises the same state machine production runs. The one seeded case (macro CUT with no PiP, asserting no PIP_STATE broadcast and unchanged from/to inputs) is a legitimate control — it passes today and pins exactly the invariant the dependent PRs (#156/#179/#180) need to not break.
APPROVE. Solid foundation for the PiP stack — happy to review #156 next now that this harness is in place.
Root of the PiP work. Three PRs build on this one:
Each lands green: every case that needs a behaviour change to pass ships with the commit that makes it pass, so no PR in the set puts a red suite on
main. Because these are fork PRs they all targetmainand each carries this commit in its own diff until it merges — so this one wants reviewing first, on its own.Why
The WS controller has no test coverage.
activation.test.tsandmacros.test.tsboth mock../ws/controller.jsout entirely, so there is nowhere to assert what a macro action sends to Strom or broadcasts to subscribers — which is most of what the controller does.What this adds
handleMessagedriven directly, with the realStromClientpointed at a throwaway HTTP server that records every request. The assertions therefore cover the URL, the verb, and the body the server actually puts on the wire. A hand-written client stand-in would have been less work, but it can drift fromStromClientwithout anything failing —vi.mockreturns an untyped object, so neithertscnor the tests would notice a changed signature. Going through the real client also exercises the bits a stand-in cannot get wrong because it never gets them right either, likeselectPreviewbeing a PUT whiletransitionis a POST.CouchDB is mocked via
vi.mock('../db/index.js'), as elsewhere in this suite.PiP state is established through real inbound messages (
SELECT_PVW_PIP,TAKE) rather than by reaching into the module-level maps, so each case exercises the same state machine the server runs in production.One case here: a macro CUT with no PiP anywhere. It pins the behaviour the PiP work must leave untouched — no
PIP_STATE, no pip-addressed preview select,from_inputandto_inputas before. It passes onmaintoday, which is what makes it a control rather than a vacuous assertion.Production change
One export:
handleMessagewas module-private and the plugin exposes only a websocket route, so there was no seam. The alternative — a fastify + websocket fixture — would drive the connect handler too and test considerably more than these paths. Happy to switch if you would rather have that; this felt like the smaller commitment to make on your behalf. Nothing else incontroller.tsmoves.Testing
npx tsc --noEmitclean.npx vitest run— the new file passes 1/1. The suite's 27 pre-existing failures inactivation.test.tsandmacros.test.tsare unchanged and present onmaintoo (all 500s from the HTTP layer).