import { expect, test } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const codecGolden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-09/sequencer-codec-probe.json"), "utf8")) as { assets: Array<{ stripType: "IMAGE" | "SOUND" | "MOVIE"; mimeType: string; path: string; byteLength: number; sha256: string }>; }; const revisionGolden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-11/sequencer-media-revision.json"), "utf8")) as { sourceSha256: string; decisions: Array<[string, number, "PUBLISH" | "STALE", string | null]>; published: string[]; cacheWrites: string[]; }; test("M11-11 gates late seek, scrub and real decode results before publish or cache", async ({ page }) => { const movie = codecGolden.assets.find((asset) => asset.stripType === "MOVIE")!; const bytes = fs.readFileSync(path.join(root, movie.path)); expect(crypto.createHash("sha256").update(bytes).digest("hex")).toBe(revisionGolden.sourceSha256); await page.goto("/"); const result = await page.evaluate(async ({ movie, bytes }) => { const [probeModule, revisionModule] = await Promise.all([ import("/src/sequencer/SequencerCodecProbe.ts"), import("/src/sequencer/SequencerMediaRevisionGate.ts"), ]); const gate = new revisionModule.SequencerMediaRevisionGate("sequencer:main", 7); const published: string[] = []; const cacheWrites: string[] = []; const decisions: Array<[string, number, string, string | null]> = []; const complete = ( request: import("/src/sequencer/SequencerMediaRevisionGate.ts").SequencerMediaRevisionRequestIR, payloadSha256 = movie.sha256, ) => ({ ...request, status: "COMPLETED" as const, sourceFrame: request.frame, payloadSha256 }); const resolve = ( request: import("/src/sequencer/SequencerMediaRevisionGate.ts").SequencerMediaRevisionRequestIR, response = complete(request), ) => { const decision = gate.resolve(request, response, (accepted) => { const label = `${accepted.operation}@${accepted.requestRevision}`; published.push(label); if (accepted.operation === "DECODE") cacheWrites.push(label); }); decisions.push([decision.operation, decision.requestRevision, decision.status, decision.code]); return decision; }; const oldSeek = gate.begin("SEEK", 10); const oldSeekResult = new Promise>((resolveResult) => setTimeout(() => resolveResult(complete(oldSeek)), 30)); const currentScrub = gate.begin("SCRUB", 20); resolve(currentScrub); resolve(oldSeek, await oldSeekResult); const oldDecode = gate.begin("DECODE", 30); const request = probeModule.createSequencerCodecProbeRequest("MOVIE", movie.mimeType, movie.byteLength, movie.sha256); const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); const lateDecode = new Promise>>((resolveReceipt) => setTimeout(() => { void probeModule.probeSequencerCodec(request, source.slice(0)).then(resolveReceipt); }, 20)); const replacedState = gate.replaceTimeline("sequencer:main", 8); const lateReceipt = await lateDecode; resolve(oldDecode, complete(oldDecode, lateReceipt.sourceSha256)); const currentDecode = gate.begin("DECODE", 40); const currentReceipt = await probeModule.probeSequencerCodec(request, source.slice(0)); resolve(currentDecode, complete(currentDecode, currentReceipt.sourceSha256)); const forgedSeek = gate.begin("SEEK", 50); resolve(forgedSeek, { ...complete(forgedSeek), requestId: "media:forged" }); let monotonicCode = ""; try { gate.replaceTimeline("sequencer:main", 8); } catch (error) { monotonicCode = (error as { code?: string }).code ?? String(error); } return { decisions, published, cacheWrites, replacedState, finalState: gate.state(), lateReceipt: { status: lateReceipt.status, backend: lateReceipt.backend }, currentReceipt: { status: currentReceipt.status, backend: currentReceipt.backend }, monotonicCode, }; }, { movie, bytes: new Uint8Array(bytes) }); expect(result.decisions).toEqual(revisionGolden.decisions); expect(result.published).toEqual(revisionGolden.published); expect(result.cacheWrites).toEqual(revisionGolden.cacheWrites); expect(result.replacedState).toEqual({ schemaVersion: 1, timelineId: "sequencer:main", timelineRevision: 8, latestRequestRevision: 4 }); expect(result.finalState).toEqual({ schemaVersion: 1, timelineId: "sequencer:main", timelineRevision: 8, latestRequestRevision: 6 }); expect(result.lateReceipt).toEqual({ status: "READY", backend: "HTML_MEDIA" }); expect(result.currentReceipt).toEqual({ status: "READY", backend: "HTML_MEDIA" }); expect(result.monotonicCode).toBe("REVISION_CONFLICT"); });