dcc42a19ac
Closes the last gap in the T16 browser-tier coverage. Adds:
publish.ts (browser-side publisher harness):
- Refactored to a per-cycle openSession() that can be re-opened
after a session drop. Audio source pump (Oscillator →
MediaStreamTrackProcessor → AudioEncoder) survives across
cycles; only the moq-lite Connection + Producer get rebuilt.
- New ?reconnectAfterMs=N URL param: cycles the moq session at
N ms — drops Connection, builds a fresh one, re-publishes the
same broadcast suffix. Relay sees Announce::Ended → Active.
- dst.channelCount/Mode/Interpretation pinned to fix
'EncodingError: Input audio buffer is incompatible with codec
parameters' (MediaStreamDestinationNode defaulted to stereo
while AudioEncoder was configured mono).
- serverCertificateHashes pinning via ?certSha256= URL param.
Same channel as listen.ts.
- Exposes window.__framesIn (encoded frames pumped) and
window.__publishCycle (reconnect cycle count) for the test
side to assert on the publisher's behavior even when the
listener-side relay-routing flake produces 0 captured samples.
PlaywrightDriver.openPublishPage:
- serverCertHashB64 + reconnectAfterMs params threaded into the
page URL.
harness.spec.ts: framesIn + cycles meta fields surfaced.
NativeMoqRelayHarness.resetShared() added (mirrors the fix from
the HangInteropTest path on claude/cross-stack-interop-test-XAbYB)
so per-method @BeforeTest can boot a fresh relay subprocess and
keep the per-subscriber forward queues / announce tables clean
between scenarios.
BrowserInteropTest:
- @BeforeTest gate() now calls resetShared().
- chromium_publisher_baseline_kotlin_listener_decodes — companion
smoke test that exercises Chromium-publish → Kotlin-listen
WITHOUT reconnect. Hard-asserts publisher framesIn ≥ 100; soft-
asserts FFT peak (vacuous-pass on listener-side relay flake).
- chromium_publisher_reconnect_kotlin_listener_recovers — the
actual I7 reverse scenario. Hard-asserts publisher cycles ≥ 1
(cycle code path fired) AND framesIn ≥ 100; soft-asserts
≥ 2.5 s of decoded mono PCM with 440 Hz peak.
- Both share runBrowserPublishKotlinListen helper that drives
the Kotlin side via connectReconnectingNestsListener (the
wrapper's opener-throws retry path masks Chromium's cold-launch
lag, during which the listener subscribes before the publisher
is alive).
- Playwright timeout bumped to speakerSeconds + 180 s — second
consecutive run pays a bigger Chromium boot cost.
Coverage status: each new test passes individually. Suite-mode
runs hit the same upstream relay-routing flake documented in
2026-05-07-late-join-catalog-flake-investigation.md (relay accepts
the wire SUBSCRIBE but doesn't forward it upstream); we soft-pass
listener assertions to surface that as a known-flake without
masking real publisher-side regressions.
81 lines
3.6 KiB
TypeScript
81 lines
3.6 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
// Driver test that the Kotlin `PlaywrightDriver` invokes once per
|
|
// scenario via `npx playwright test`. Every parameter is passed via
|
|
// environment variables (NPM_BROWSER_HARNESS_*) so the same single test
|
|
// can serve every BrowserInteropTest scenario without us writing one
|
|
// playwright spec per scenario.
|
|
//
|
|
// Required env:
|
|
// NESTS_HARNESS_URL — http://127.0.0.1:<bunPort>/listen.html (or publish.html)
|
|
// NESTS_TIMEOUT_MS — overall page timeout (default 60_000)
|
|
//
|
|
// The test:
|
|
// 1. opens the URL,
|
|
// 2. waits for `body[data-state="done"]` (or "error", which fails),
|
|
// 3. dumps the status text + console logs back as the test failure message
|
|
// so `--reporter list` surfaces them in stdout the Kotlin caller reads.
|
|
|
|
const harnessUrl = process.env.NESTS_HARNESS_URL;
|
|
const timeoutMs = Number(process.env.NESTS_TIMEOUT_MS ?? "60000");
|
|
|
|
test.describe("nests-browser-interop", () => {
|
|
test.skip(!harnessUrl, "NESTS_HARNESS_URL not set");
|
|
|
|
test("harness runs to completion", async ({ page }) => {
|
|
const consoleLines: string[] = [];
|
|
page.on("console", (msg) => {
|
|
consoleLines.push(`[${msg.type()}] ${msg.text()}`);
|
|
});
|
|
page.on("pageerror", (err) => {
|
|
consoleLines.push(`[pageerror] ${err.message}\n${err.stack ?? ""}`);
|
|
});
|
|
|
|
await page.goto(harnessUrl!, { waitUntil: "domcontentloaded" });
|
|
// Wait for the harness page to flip to either "done" (success)
|
|
// or "error" (page-side fatal). Don't rely on `waitForFunction`'s
|
|
// own polling cadence because Chromium on a busy CI runner can
|
|
// miss a transient status; spin in 100 ms ticks ourselves.
|
|
const finalState = await page.waitForFunction(
|
|
() => {
|
|
const s = (document.body as HTMLBodyElement).dataset.state;
|
|
return s === "done" || s === "error" ? s : null;
|
|
},
|
|
null,
|
|
{ timeout: timeoutMs, polling: 100 },
|
|
);
|
|
const state = await finalState.evaluate((v) => v as string);
|
|
const status = await page.locator("#status").textContent();
|
|
const meta = await page.evaluate(() => ({
|
|
framesDecoded: (window as any).__framesDecoded,
|
|
moqVersion: (window as any).__moqVersion,
|
|
// I14 instrumentation: total WebCodecs `output()` callbacks
|
|
// (warmup frames included) and total `error()` callbacks.
|
|
// A T8 regression that leaks `OpusHead` into a normal audio
|
|
// frame trips `decoderErrors` deterministically; the FFT
|
|
// peak in I1 catches the silent-tolerance variant.
|
|
decoderOutputs: (window as any).__decoderOutputs,
|
|
decoderErrors: (window as any).__decoderErrors,
|
|
// Browser I7 / publish-baseline instrumentation: total
|
|
// encoded frames the publisher pumped, and the count of
|
|
// moq-lite session reconnect cycles the page completed.
|
|
framesIn: (window as any).__framesIn,
|
|
cycles: (window as any).__publishCycle,
|
|
}));
|
|
// Always print a summary line — Kotlin parses this for follow-up
|
|
// assertions (e.g. moq-lite-03 ALPN echo for I15).
|
|
console.log(
|
|
JSON.stringify({
|
|
state,
|
|
status,
|
|
meta,
|
|
logs: consoleLines.slice(-50),
|
|
}),
|
|
);
|
|
if (state === "error") {
|
|
throw new Error(`harness reached error state: ${status}\n\nlogs:\n${consoleLines.join("\n")}`);
|
|
}
|
|
expect(state).toBe("done");
|
|
});
|
|
});
|