121 lines
5.2 KiB
JavaScript
121 lines
5.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, "../../..");
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "sequencer-media-cache-unit-"));
|
|
|
|
function transpile(sourceName, outputName, replacements = []) {
|
|
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
|
|
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
|
fileName: sourcePath,
|
|
reportDiagnostics: true,
|
|
});
|
|
assert.deepEqual(result.diagnostics, []);
|
|
fs.writeFileSync(path.join(temporary, outputName), replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText));
|
|
}
|
|
|
|
transpile("capability-gates.ts", "capability-gates.mjs");
|
|
transpile("asset-path.ts", "asset-path.mjs");
|
|
transpile("sequencer.ts", "sequencer.mjs", [
|
|
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
|
|
['from "./asset-path"', 'from "./asset-path.mjs"'],
|
|
]);
|
|
transpile("sequencer-media-cache.ts", "sequencer-media-cache.mjs", [
|
|
['from "./sequencer"', 'from "./sequencer.mjs"'],
|
|
]);
|
|
const mediaCache = await import(pathToFileURL(path.join(temporary, "sequencer-media-cache.mjs")));
|
|
|
|
const source = {
|
|
schemaVersion: 1,
|
|
stripType: "MOVIE",
|
|
mimeType: "video/mp4",
|
|
byteLength: 1_484,
|
|
sourceSha256: "a".repeat(64),
|
|
};
|
|
const capability = {
|
|
...source,
|
|
status: "READY",
|
|
backend: "HTML_MEDIA",
|
|
reason: null,
|
|
decoded: { width: 16, height: 16, durationMicros: 1_000_000 },
|
|
};
|
|
const profile = {
|
|
kind: "MOVIE_RGBA8_FRAME",
|
|
width: 2,
|
|
height: 2,
|
|
colorSpace: "SRGB8",
|
|
alphaMode: "STRAIGHT",
|
|
};
|
|
const payload = new Uint8Array([
|
|
1, 2, 3, 255, 4, 5, 6, 255,
|
|
7, 8, 9, 255, 10, 11, 12, 255,
|
|
]).buffer;
|
|
|
|
test("M11-10 binds proxy identity to source, decode receipt, profile and source frame", async () => {
|
|
const manifest = await mediaCache.createSequencerMediaCacheManifest(source, capability, profile, 12, payload);
|
|
assert.equal(manifest.payloadByteLength, 16);
|
|
assert.match(manifest.identitySha256, /^[a-f0-9]{64}$/);
|
|
assert.match(manifest.payloadSha256, /^[a-f0-9]{64}$/);
|
|
assert.equal(mediaCache.sequencerMediaCacheKey(manifest), `sequencer-media-cache:v1:${manifest.identitySha256}`);
|
|
assert.deepEqual(await mediaCache.verifySequencerMediaCacheEntry(manifest, payload, source, capability), manifest);
|
|
|
|
const reordered = { ...capability, decoded: { durationMicros: 1_000_000, height: 16, width: 16 } };
|
|
assert.equal(
|
|
await mediaCache.computeSequencerMediaCacheIdentity(source, capability, profile, 12),
|
|
await mediaCache.computeSequencerMediaCacheIdentity(source, reordered, profile, 12),
|
|
);
|
|
assert.notEqual(
|
|
await mediaCache.computeSequencerMediaCacheIdentity(source, capability, profile, 12),
|
|
await mediaCache.computeSequencerMediaCacheIdentity(source, capability, profile, 13),
|
|
);
|
|
});
|
|
|
|
test("M11-10 rejects source, decode capability, identity and payload drift independently", async () => {
|
|
const manifest = await mediaCache.createSequencerMediaCacheManifest(source, capability, profile, 0, payload);
|
|
const changedSource = { ...source, sourceSha256: "b".repeat(64) };
|
|
const changedSourceCapability = { ...capability, sourceSha256: changedSource.sourceSha256 };
|
|
await assert.rejects(
|
|
mediaCache.verifySequencerMediaCacheEntry(manifest, payload, changedSource, changedSourceCapability),
|
|
{ code: "SEQUENCER_CACHE_SOURCE_MISMATCH" },
|
|
);
|
|
const changedCapability = { ...capability, decoded: { ...capability.decoded, durationMicros: 999_999 } };
|
|
await assert.rejects(
|
|
mediaCache.verifySequencerMediaCacheEntry(manifest, payload, source, changedCapability),
|
|
{ code: "SEQUENCER_CACHE_CAPABILITY_MISMATCH" },
|
|
);
|
|
await assert.rejects(
|
|
mediaCache.verifySequencerMediaCacheEntry({ ...manifest, identitySha256: "c".repeat(64) }, payload, source, capability),
|
|
{ code: "SEQUENCER_CACHE_IDENTITY_MISMATCH" },
|
|
);
|
|
const corrupted = payload.slice(0);
|
|
new Uint8Array(corrupted)[0] ^= 0xff;
|
|
await assert.rejects(
|
|
mediaCache.verifySequencerMediaCacheEntry(manifest, corrupted, source, capability),
|
|
{ code: "SEQUENCER_CACHE_HASH_MISMATCH" },
|
|
);
|
|
});
|
|
|
|
test("M11-10 keeps blocked receipts, extension fields and oversized profiles out of cache", async () => {
|
|
await assert.rejects(
|
|
mediaCache.createSequencerMediaCacheManifest(source, { ...capability, status: "BLOCKED", backend: null, reason: "RUNTIME_UNAVAILABLE", decoded: null }, profile, 0, payload),
|
|
{ code: "SEQUENCER_CODEC_UNSUPPORTED" },
|
|
);
|
|
assert.throws(() => mediaCache.parseSequencerMediaProxyProfile({ ...profile, codec: "h264" }), { code: "SEQUENCER_SCHEMA_INVALID" });
|
|
assert.throws(
|
|
() => mediaCache.parseSequencerMediaProxyProfile({ ...profile, width: 4096, height: 4097 }),
|
|
{ code: "SEQUENCER_BUDGET_EXCEEDED" },
|
|
);
|
|
await assert.rejects(
|
|
mediaCache.createSequencerMediaCacheManifest(source, capability, { ...profile, width: 17 }, 0, new ArrayBuffer(17 * 2 * 4)),
|
|
{ code: "SEQUENCER_SCHEMA_INVALID" },
|
|
);
|
|
});
|
|
|
|
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));
|