Skip to content

test(ws): cover PiP handling in macro CUT, TRANSITION, and TAKE - #181

Closed
wagenet wants to merge 3 commits into
Eyevinn:mainfrom
wagenet:wagenet/ws-macro-pip-tests
Closed

wagenet wants to merge 3 commits into
Eyevinn:mainfrom
wagenet:wagenet/ws-macro-pip-tests

Conversation

@wagenet

@wagenet wagenet commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Builds on #179 and #180. The diff contains all three commits until those merge — only the third, test(ws): cover PiP handling in macro CUT, TRANSITION, and TAKE, belongs to this PR.

Why

The macro PiP paths had no coverage, and neither does the WS controller as a whole — activation.test.ts and macros.test.ts both mock ../ws/controller.js out entirely. #179 and #180 rested on reading the interactive handlers and mirroring them, which is exactly the kind of change that deserves execution behind it.

What it does

Drives handleMessage with CouchDB and StromClient mocked, asserting the Strom call sequence and the broadcast payloads. 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.

Four cases:

Case Asserts
macro CUT over a PGM PiP PiP moves to preview, PIP_STATE emitted, selectPreview({pip:0}) called, from_input is the tracked background
macro TRANSITION over a PGM PiP same displacement and restore
macro TAKE with a PiP in preview Strom is called at all, PiP reaches program
macro CUT with no PiP no PIP_STATE, no pip-addressed selectPreview, tally unchanged

It discriminates

A test that passes against broken code is worth nothing, so each case was run against the unfixed trees:

against main (neither fix)    3 fail, control passes
with #179 only                1 fails — the preview-PiP take
with both fixes               4 pass

The no-PiP control passes on all three, which is what makes it a control rather than a vacuous assertion. The single failure against #179 is precisely the bug #180 fixes.

Production change

One export:

/** Exported for tests: drives one inbound message against a production. */
export async function handleMessage(

handleMessage was module-private and the plugin only exposes a websocket route, so there was no seam. The alternative — a fastify + websocket fixture — would drive the connect handler too and test far more than these paths. If you would rather have that, I am happy to switch; this felt like the smaller commitment to make on your behalf. Nothing else in controller.ts moves.

Testing

  • npx tsc --noEmit clean.
  • npx vitest run — the new file passes 4/4. The suite's 27 pre-existing failures in activation.test.ts and macros.test.ts are unchanged (all 500s from the HTTP layer, present on main too).

Still not covered: anything requiring a real mixer. Strom builds and runs locally, but neither of its sample flows carries a vision mixer block and its own suite has no PiP coverage either, so selectPreview({pip:N}) setting pvw_pip such that the next transition takes the PiP to program remains supported by reading flows.rs and mixer_ops.rs rather than by execution.

wagenet and others added 3 commits August 27, 2026 15:55
The CUT and TRANSITION handlers each do four things when a real source
replaces a PiP that is on program: resolve `from_input` from
`pgmBgByProduction`, move the PiP from PGM to PVW, broadcast `PIP_STATE`,
and call `selectPreview` so the PiP lands back in Strom's preview.

The same three actions executed from a macro do none of it. `MACRO_EXEC`
updates `tally` and calls `stromTransition` directly, so with a PiP on
program the PiP is never returned to preview, `pgmPipByProduction` and
`pgmBgByProduction` keep their old values, and no `PIP_STATE` is emitted —
leaving both the server and every subscriber believing a PiP is on program
long after it left.

The `selectPreview` omission is the one that reaches the mixer. Strom's
`trigger_transition` reads the authoritative PGM/PVW source from the block's
overlay state, which `selectPreview` is what sets. Without the restore,
preview holds the cut target that `stromTransition` selected rather than the
displaced PiP, so the operator's next take airs the wrong source.

The frame airing at the moment the macro runs is unaffected: the cut target
comes from `stromTransition`'s own `selectPreview`, which is untouched here.

Passing `fromPad` instead of `tally.pgm` is included for consistency with the
interactive handlers, not as a fix. Strom ignores the request's
`from_input`/`to_input` whenever overlay state exists, falling back to them
only when it is missing entirely.

Mirrors the interactive logic into the three macro actions. The displacement
block is inlined rather than extracted so each macro path can be diffed
against the handler it copies; the interactive handlers are left untouched.

A PiP sitting in *preview* is still not promoted to program by a macro TAKE
— that needs the full TAKE path, which tracks `pvwBeforePipByProduction` and
runs its own Strom sequence. This commit only covers displacing a PiP that is
already on program.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
A macro TAKE never promoted a PiP sitting in preview, and the failure was
silent. `SELECT_PVW_PIP` leaves `tally.pvw` null, so the swap produced
`{ pgm: null, pvw: <old pgm> }`, which was broadcast as-is and then passed to
`stromTransition` as `toMixerInput` — where it early-returns on the null.

The result: every subscriber is told the program bus is empty, Strom is never
called at all and keeps airing the previous source, and `pgmPipByProduction`
is never set, so the server does not know the PiP took either.

Restructures the macro TAKE to mirror the interactive handler's three
branches — PiP moving PVW to PGM, PiP moving PGM to PVW, and no PiP — instead
of the single displacement path that only covered the second. The pip maps
are swapped as a pair (`newPgmPip = curPvwPip`, `newPvwPip = curPgmPip`)
rather than cleared, and `PIP_STATE` is emitted unconditionally, matching the
interactive TAKE.

The forward branch reuses `pvwBeforePipByProduction` as `to_input` and records
it as the new `pgmBg`, so a later CUT or TRANSITION while the PiP is on
program has a background to report and Strom keeps `pgm_input ≠ pvw_input`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
The macro PiP paths had no coverage, and neither did the WS controller as a
whole — the existing suites mock `../ws/controller.js` out entirely. Both
preceding commits therefore rested on inspection alone.

Drives `handleMessage` with CouchDB and StromClient mocked, asserting the
Strom call sequence and the broadcast payloads. 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.

Four cases: a macro CUT and a macro TRANSITION over a PiP on program, a macro
TAKE promoting a PiP sitting in preview, and a macro CUT with no PiP anywhere
as a control.

Verified to discriminate rather than pass vacuously:

  against main (neither fix)   3 fail, control passes
  with the displacement fix    1 fails — the preview-PiP take
  with both fixes              4 pass

`handleMessage` is exported for this. It is the only production change here,
and nothing else in the file moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
@wagenet

wagenet commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Absorbed into the stack. The harness is now #182 at the root, and each fix ships with the cases it makes pass — #179 carries the two displacement cases, #180 the preview-PiP take. That way every PR is green on arrival instead of two behaviour changes landing ahead of their tests.

@wagenet wagenet closed this Aug 28, 2026
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.

1 participant