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; decoded: Record; }>; }; const cacheGolden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-10/sequencer-media-cache.json"), "utf8")) as { sourceSha256: string; profile: { kind: "MOVIE_RGBA8_FRAME"; width: number; height: number; colorSpace: "SRGB8"; alphaMode: "STRAIGHT" }; identitySha256: string; proxyByteLength: number; }; test("M11-10 binds a real movie proxy cache to source hash and runtime decode capability", 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(cacheGolden.sourceSha256); await page.goto("/"); const result = await page.evaluate(async ({ movie, profile, bytes }) => { const [probeModule, cacheModule, storageModule] = await Promise.all([ import("/src/sequencer/SequencerCodecProbe.ts"), import("/src/sequencer/SequencerMediaProxyCache.ts"), import("/src/storage/StorageClient.ts"), ]); const request = probeModule.createSequencerCodecProbeRequest("MOVIE", movie.mimeType, movie.byteLength, movie.sha256); const sourceData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); const capability = await probeModule.probeSequencerCodec(request, sourceData.slice(0)); const generated = await cacheModule.generateInitialSequencerMovieProxyFrame(request, capability, profile, sourceData.slice(0)); const cache = new cacheModule.SequencerMediaProxyCache(generated.data.byteLength); const firstKey = await cache.put(generated.manifest, generated.data.slice(0), request, capability); const hit = await cache.get(request, capability, profile, 0); const secondManifest = await import("/src/sequencer/SequencerTimeline.ts").then((module) => module.createSequencerMediaCacheManifest(request, capability, profile, 1, generated.data.slice(0))); const secondKey = await cache.put(secondManifest, generated.data.slice(0), request, capability); const evicted = await cache.get(request, capability, profile, 0); const projectId = `m11-10-${Date.now()}`; const writer = new storageModule.StorageClient(); const storedPayload = await writer.putAsset( projectId, generated.data.slice(0), "application/vnd.blender.sequencer-proxy-rgba8", "cache/sequencer/proxy-frame-0.rgba8", ); const manifestData = new TextEncoder().encode(JSON.stringify(generated.manifest)).buffer as ArrayBuffer; const storedManifest = await writer.putAsset( projectId, manifestData, "application/vnd.blender.sequencer-proxy-cache+json", "cache/sequencer/proxy-frame-0.json", ); writer.terminate(); const reader = new storageModule.StorageClient(); const [reopenedPayload, reopenedManifestAsset] = await Promise.all([ reader.readAsset(projectId, storedPayload.sha256), reader.readAsset(projectId, storedManifest.sha256), ]); reader.terminate(); const reopenedManifest = JSON.parse(new TextDecoder().decode(reopenedManifestAsset.data)); const verified = await import("/src/sequencer/SequencerTimeline.ts").then((module) => module.verifySequencerMediaCacheEntry(reopenedManifest, reopenedPayload.data, request, capability)); const changedSource = { ...request, sourceSha256: "e".repeat(64) }; const changedSourceCapability = { ...capability, sourceSha256: changedSource.sourceSha256 }; const changedCapability = { ...capability, decoded: { ...capability.decoded!, durationMicros: capability.decoded!.durationMicros! + 1 }, }; const corruptPayload = reopenedPayload.data.slice(0); new Uint8Array(corruptPayload)[0] ^= 0xff; const code = async (operation: () => Promise): Promise => { try { await operation(); return "unexpected-success"; } catch (error) { return (error as { code?: string }).code ?? String(error); } }; const protocol = await import("/src/sequencer/SequencerTimeline.ts"); const errors = { source: await code(() => protocol.verifySequencerMediaCacheEntry(reopenedManifest, reopenedPayload.data, changedSource, changedSourceCapability)), capability: await code(() => protocol.verifySequencerMediaCacheEntry(reopenedManifest, reopenedPayload.data, request, changedCapability)), payload: await code(() => protocol.verifySequencerMediaCacheEntry(reopenedManifest, corruptPayload, request, capability)), sourceBytes: await code(() => cacheModule.generateInitialSequencerMovieProxyFrame(request, capability, profile, new ArrayBuffer(request.byteLength))), }; const statsBeforeClear = cache.stats(); const releasedBytes = cache.clear(); return { capability, manifest: generated.manifest, firstKey, secondKey, hitBytes: hit?.data.byteLength, evicted: evicted === undefined, statsBeforeClear, releasedBytes, statsAfterClear: cache.stats(), storedPayloadSha256: storedPayload.sha256, verifiedIdentity: verified.identitySha256, errors, }; }, { movie, profile: cacheGolden.profile, bytes: new Uint8Array(bytes) }); expect(result.capability).toMatchObject({ status: "READY", backend: "HTML_MEDIA", decoded: movie.decoded }); expect(result.manifest).toMatchObject({ identitySha256: cacheGolden.identitySha256, payloadByteLength: cacheGolden.proxyByteLength, profile: cacheGolden.profile, }); expect(result.manifest.payloadSha256).toBe(result.storedPayloadSha256); expect(result.verifiedIdentity).toBe(cacheGolden.identitySha256); expect(result.firstKey).toBe(`sequencer-media-cache:v1:${cacheGolden.identitySha256}`); expect(result.secondKey).not.toBe(result.firstKey); expect(result.hitBytes).toBe(cacheGolden.proxyByteLength); expect(result.evicted).toBe(true); expect(result.statsBeforeClear).toMatchObject({ entries: 1, bytes: cacheGolden.proxyByteLength, hits: 1, misses: 1, evictions: 1 }); expect(result.releasedBytes).toBe(cacheGolden.proxyByteLength); expect(result.statsAfterClear).toMatchObject({ entries: 0, bytes: 0 }); expect(result.errors).toEqual({ source: "SEQUENCER_CACHE_SOURCE_MISMATCH", capability: "SEQUENCER_CACHE_CAPABILITY_MISMATCH", payload: "SEQUENCER_CACHE_HASH_MISMATCH", sourceBytes: "SEQUENCER_CACHE_SOURCE_MISMATCH", }); });