Conversation
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
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsandmacros.test.tsboth mock../ws/controller.jsout 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
handleMessagewith CouchDB andStromClientmocked, 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:
PIP_STATEemitted,selectPreview({pip:0})called,from_inputis the tracked backgroundPIP_STATE, no pip-addressedselectPreview, tally unchangedIt discriminates
A test that passes against broken code is worth nothing, so each case was run against the unfixed trees:
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:
handleMessagewas 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 incontroller.tsmoves.Testing
npx tsc --noEmitclean.npx vitest run— the new file passes 4/4. The suite's 27 pre-existing failures inactivation.test.tsandmacros.test.tsare unchanged (all 500s from the HTTP layer, present onmaintoo).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})settingpvw_pipsuch that the next transition takes the PiP to program remains supported by readingflows.rsandmixer_ops.rsrather than by execution.