From 063dde8b82ea34e1aa9d5ae8340b685c3405fb8e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 21:01:15 -0800 Subject: [PATCH 1/5] chore(quest): claim quest/m0/interop-green/open-gop-fixture.md Quest-Claim: cd51d356b85cec1c From 23c971b9e0a6fed34b64dff2dd60dc880f3c4564 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 21:07:20 -0800 Subject: [PATCH 2/5] fix(test): type open-GOP fixture and surface page errors Co-Authored-By: GPT-6.1 Sol --- quest/m0/interop-green/README.md | 4 +- quest/m0/interop-green/open-gop-fixture.md | 43 +++++++++++---------- test/interop/clients/js/open-gop.browser.ts | 39 +++++++++++++------ test/interop/clients/js/open-gop/main.ts | 26 +++++++------ 4 files changed, 66 insertions(+), 46 deletions(-) diff --git a/quest/m0/interop-green/README.md b/quest/m0/interop-green/README.md index 3f8ba886edc..c10bbed5e4f 100644 --- a/quest/m0/interop-green/README.md +++ b/quest/m0/interop-green/README.md @@ -37,7 +37,7 @@ the fallback audio stall, which hit on [41668668824](https://github.com/moq-dev/moq/actions/runs/41668668824); see [the IETF22 fallback quest](/quest/m0/test-flakes-2/ietf22-fallback.md). -Every child has landed. Video passed its scheduled +Video passed its scheduled [run 41497602703](https://github.com/moq-dev/moq/actions/runs/41497602703). Interop runs [41317901057](https://github.com/moq-dev/moq/actions/runs/41317901057), @@ -68,4 +68,4 @@ Public API: none. Wire: none. ## Required -- [The open-GOP recovery fixture provides the broadcast name](/quest/m0/interop-green/open-gop-fixture.md) - the open-GOP browser fixture stubs `in.name`, and an exception in an effect fails the test at once instead of timing out +- [Confirm scheduled open-GOP recovery passes](/quest/m0/interop-green/open-gop-fixture.md) - a scheduled Interop run passes the open-GOP case with the typed fixture and direct effect/page error reporting diff --git a/quest/m0/interop-green/open-gop-fixture.md b/quest/m0/interop-green/open-gop-fixture.md index e13138384ab..2e692cbfd41 100644 --- a/quest/m0/interop-green/open-gop-fixture.md +++ b/quest/m0/interop-green/open-gop-fixture.md @@ -1,13 +1,20 @@ -# [XS] The open-GOP recovery fixture provides the broadcast name +# [XS] Confirm scheduled open-GOP recovery passes ## Goal The scheduled Interop run's "Harness regressions" step passes the `open-gop.browser.ts` case "software video recovery trims orphan pictures -after join and later gap". The fixture's fake broadcast provides everything -`@moq/watch`'s decoder reads. From now on, an exception thrown inside a -fixture's effect fails the test immediately with that error, instead of -surfacing as a 60-second timeout. +after join and later gap" on `main` with the typed fixture and direct +reporting of effect/page exceptions. + +## Waiting + +- moq-dev/moq: a scheduled Interop run on `main` including + [moq-dev/moq#7814](https://github.com/moq-dev/moq/pull/7814) passes the + open-GOP recovery case. Check with + `gh run list --repo moq-dev/moq --workflow interop.yml --event schedule`, + then inspect its "Harness regressions" step. Delete this quest once that + case passes; the parent epic owns the whole-workflow pass. ## Plan @@ -16,21 +23,15 @@ Decided 2026-11-03 (maintainer, /quest-plan of the follow-ups from [moq-dev/moq#7716](https://github.com/moq-dev/moq/pull/7716)): milestone m0, a child of the Interop epic, since it breaks the scheduled run. -- Cause: [moq-dev/moq#7222](https://github.com/moq-dev/moq/pull/7222) - (`2738588cfc`) made `js/watch/src/video/decoder.ts` read - `broadcast.in.name.peek()` for stats. The fake broadcast in - `test/interop/clients/js/open-gop/main.ts` provides only `out: { catalog, - active }` and `relativeBroadcast`, so the read throws inside an effect. - The scheduled run - [41668668824](https://github.com/moq-dev/moq/actions/runs/41668668824) - fails with `TypeError: Cannot read properties of undefined (reading - 'name')` at `window.runRecovery`. -- Add `in: { name }` to the stub, typed against the real broadcast's shape - where the fixture allows it, so the next field the decoder reads fails - type-checking instead of at run time. -- Fail fast: route page errors and effect exceptions to a test failure. - [moq-dev/moq#7716](https://github.com/moq-dev/moq/pull/7716) adds - page-error collection to `harness.browser.ts`. Reuse it if it has landed, - or add the same collection here and let #7716 merge onto it. +[moq-dev/moq#7672](https://github.com/moq-dev/moq/pull/7672) supplied the +missing broadcast name. [moq-dev/moq#7814](https://github.com/moq-dev/moq/pull/7814) +replaces the unchecked broadcast stub with a real local origin and a +manual-catalog `Broadcast`. It reuses the browser harness's existing page +and console error collection while polling recovery, so an effect exception +fails directly even when the recovery promise never settles. The recovery +assertions and 60-second deadline remain unchanged. + +Only scheduled evidence remains. Do not dispatch Nightly manually or treat +a local pass as scheduled evidence. Triage a remaining failure at its cause. Public API: none. Wire: none. diff --git a/test/interop/clients/js/open-gop.browser.ts b/test/interop/clients/js/open-gop.browser.ts index c4f00b61196..930c41afcce 100644 --- a/test/interop/clients/js/open-gop.browser.ts +++ b/test/interop/clients/js/open-gop.browser.ts @@ -5,10 +5,17 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { build } from "vite"; import { worklet } from "../../../../js/common/vite-plugin-worklet"; -import { launch } from "./harness"; +import { launch, open, throwPageErrors, waitFor } from "./harness"; import type { Packet } from "./open-gop/main"; +declare global { + interface Window { + recoveryResult?: Awaited>; + } +} + test("software video recovery trims orphan pictures after join and later gap", async () => { + const deadline = Date.now() + 60_000; const run = mkdtempSync(join(tmpdir(), "moq-open-gop-")); const file = join(run, "fixture.ts"); const command = (cmd: string[]) => { @@ -104,17 +111,27 @@ test("software video recovery trims orphan pictures after join and later gap", a return (await file.exists()) ? new Response(file) : new Response("not found", { status: 404 }); }, }); - const page = await browser.newPage(); - page.on("pageerror", (error) => console.error(error)); - page.on("console", (message) => { - if (message.type() === "error") console.error(message.text()); + const [page, errors] = await open(browser, `http://127.0.0.1:${server.port}`, "open-gop"); + await waitFor(page, errors, (page) => page.evaluate(() => typeof window.runRecovery === "function"), { + deadline, + description: "the recovery fixture to load", + predicate: (ready) => ready, + }); + await page.evaluate((packets) => { + // Keep page reads responsive while recovery runs, so an effect error can end the wait. + void window.runRecovery(packets).then((result) => { + window.recoveryResult = result; + }); + }, packets); + const result = await waitFor(page, errors, (page) => page.evaluate(() => window.recoveryResult), { + deadline, + description: "open-GOP recovery", + predicate: (result) => result !== undefined, }); - await page.goto(`http://127.0.0.1:${server.port}`); - await page.waitForFunction(() => typeof window.runRecovery === "function"); - const result = await page.evaluate((packets) => window.runRecovery(packets), packets); - expect(result.software).toBe(true); - expect(result.submitted).toEqual(expected); - expect(result.decoded).toBeGreaterThan(0); + throwPageErrors(errors); + expect(result?.software).toBe(true); + expect(result?.submitted).toEqual(expected); + expect(result?.decoded).toBeGreaterThan(0); } finally { await browser.close(); server?.stop(); diff --git a/test/interop/clients/js/open-gop/main.ts b/test/interop/clients/js/open-gop/main.ts index de9f7d28240..71e367ae54c 100644 --- a/test/interop/clients/js/open-gop/main.ts +++ b/test/interop/clients/js/open-gop/main.ts @@ -2,8 +2,7 @@ import { Micro, Milli } from "@moq/clock"; import * as Catalog from "@moq/hang/catalog"; import * as Container from "@moq/hang/container"; import * as Moq from "@moq/net"; -import { Signal } from "@moq/signals"; -import type { Broadcast } from "../../../../../js/watch/src/broadcast"; +import { Broadcast } from "../../../../../js/watch/src/broadcast"; import { Sync } from "../../../../../js/watch/src/sync"; import { Decoder } from "../../../../../js/watch/src/video/decoder"; import { Source } from "../../../../../js/watch/src/video/source"; @@ -59,7 +58,11 @@ window.runRecovery = async (packets) => { super.decode(chunk); } }; - const producer = new Moq.Broadcast.Producer(); + const origin = new Moq.Origin.Producer(); + const name = Moq.Path.from("open-gop"); + const publication = origin.createBroadcast(name); + publication.announce(); + const producer = publication.broadcast; const track = producer.createTrack("video", { timescale: Moq.Time.Timescale.MICRO }); let group: Moq.Group.Producer | undefined; for (const packet of packets) { @@ -77,19 +80,18 @@ window.runRecovery = async (packets) => { group?.finish(); track.finish(); track.close(); - const media = producer.consume(); const config = Catalog.VideoConfigSchema.parse({ codec: "avc1.64001f", container: { kind: "legacy" }, codedWidth: 160, codedHeight: 96, }); - const catalog = new Signal({ video: { renditions: { video: config } } }); - const broadcast = new Signal({ - in: { name: new Signal(Moq.Path.from("open-gop")) }, - out: { catalog, active: new Signal(media) }, - relativeBroadcast: () => media, - } as unknown as Broadcast); + const broadcast = new Broadcast({ + origin, + name, + catalogFormat: "manual", + catalog: { video: { renditions: { video: config } } }, + }); const source = new Source({ broadcast, supported: async () => true }); const sync = new Sync({ delay: Milli(0), buffer: Milli(10_000) }); const decoder = new Decoder({ source, sync }); @@ -103,8 +105,8 @@ window.runRecovery = async (packets) => { decoder.close(); source.close(); sync.close(); - media.close(); - producer.close(); + broadcast.close(); + origin.close(); window.VideoDecoder = Native; Container.Consumer.prototype.next = next; } From 3ccdf9cab4fe61a2b0fe99bb51e5964990dc3dc5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 4 Nov 2026 10:00:59 -0800 Subject: [PATCH 3/5] chore(quest): claim quest/m0/interop-green/open-gop-fixture.md Quest-Claim: 7d972291608ffb2c From 67f388742eb9f82383f63ebeefca0cc08c74816d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 4 Nov 2026 10:02:33 -0800 Subject: [PATCH 4/5] quest: close the open-GOP fixture quest after scheduled run 41863118968 Co-Authored-By: Claude Opus 5.5 --- quest/m0/interop-green/README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/quest/m0/interop-green/README.md b/quest/m0/interop-green/README.md index f31e89a3e1a..32a34d7b31b 100644 --- a/quest/m0/interop-green/README.md +++ b/quest/m0/interop-green/README.md @@ -1,4 +1,4 @@ -# Interop and Video pass on main +# [XS] Interop and Video pass on main ## Goal @@ -8,9 +8,7 @@ means a new break. Interop has failed every scheduled run since ## Waiting -- moq-dev/moq: a scheduled Interop run on `main` passes, once this quest's - fixture fixes, wire-compat remeasure, and 90-minute job budget have - landed. Check with +- moq-dev/moq: a scheduled Interop run on `main` passes. Check with `gh run list --repo moq-dev/moq --workflow interop.yml --event schedule`. Triage any red run, then delete this quest once one passes. @@ -29,8 +27,7 @@ port-reservation lines are negative self-tests in `interop/relay-log.test.sh` and `lib/harness.test.sh` that pass. Already owned: the intermittent media-matrix timestamp-gap cells (`cpp -> js-native-node`, `go -> c`, browser `rust -> js` timeouts) belong to -[test flakes](/quest/m0/test-flakes-2/README.md), with -[moq-dev/moq#6990](https://github.com/moq-dev/moq/pull/6990) open. So does +[test flakes](/quest/m0/test-flakes-2/README.md). So does the fallback audio stall, which hit on `moq-lite-07-wip` in [41495461904](https://github.com/moq-dev/moq/actions/runs/41495461904) and @@ -64,4 +61,11 @@ showed more causes, fixed in this quest's own PR: The P2P data-channel test timed out once, in 41668668824, and passes locally; triage it if it recurs. +Scheduled run +[41863118968](https://github.com/moq-dev/moq/actions/runs/41863118968) +(2026-11-04, after every fix above landed) passed the open-GOP recovery +case, P2P, and wire-compat. Its only failure was the fallback audio stall +above, on `moq-lite-07-wip` in Chromium, which the IETF22 fallback quest +owns. + Public API: none. Wire: none. From 246628a6e1f6d8843b70cd716dd57609601be45e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 4 Nov 2026 10:36:00 -0800 Subject: [PATCH 5/5] quest: record the Chromium fallback stalls in the IETF22 fallback quest Scheduled Interop runs 41495461904, 41668668824, and 41863118968 all hit the fallback audio stall in Chromium on old-moq-lite-07-wip, so the quest's Goal no longer limits the moq-lite case to Firefox. Co-Authored-By: Claude Opus 5.5 --- quest/m0/test-flakes-2/ietf22-fallback.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/quest/m0/test-flakes-2/ietf22-fallback.md b/quest/m0/test-flakes-2/ietf22-fallback.md index b7c7643087d..e7d5fc0b962 100644 --- a/quest/m0/test-flakes-2/ietf22-fallback.md +++ b/quest/m0/test-flakes-2/ietf22-fallback.md @@ -3,7 +3,7 @@ ## Goal When the newest of two live publishers withdraws, a browser `@moq/watch` -viewer on moq-transport-22, or on moq-lite in Firefox, plays the still-live +viewer on moq-transport-22 or moq-lite plays the still-live older run within the media lane's derived 1039 ms bound. It keeps its session, and its audio resumes with its video. Both failures are fixed at their cause once one recurs with enough evidence to reproduce it deterministically. @@ -30,7 +30,11 @@ after the fallback), on `old-moq-lite-07-wip`: and [2026-11-03](https://github.com/moq-dev/moq/actions/runs/41656613559/job/125988510133). [moq-dev/moq#7716](https://github.com/moq-dev/moq/pull/7716) found it -separate from the Firefox tone problem. Read those jobs' artifacts first. If +separate from the Firefox tone problem. Scheduled Interop runs hit the same +stall in Chromium, also on `old-moq-lite-07-wip`: +[41495461904](https://github.com/moq-dev/moq/actions/runs/41495461904), +[41668668824](https://github.com/moq-dev/moq/actions/runs/41668668824), and +[41863118968](https://github.com/moq-dev/moq/actions/runs/41863118968). Read those jobs' artifacts first. If they isolate the cause, the Waiting item clears. If they don't, rerun the Waiting section's command with `--media-engines firefox` and the same `RUST_LOG` filter.