95 lines
4.9 KiB
TypeScript
95 lines
4.9 KiB
TypeScript
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<ReturnType<typeof complete>>((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<Awaited<ReturnType<typeof probeModule.probeSequencerCodec>>>((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");
|
|
});
|