import { expect, test } from "@playwright/test"; test("M10-05 isolates Simulation caches by committed graph/source/revision identity", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const result = await page.evaluate(async () => { const { StorageClient } = await import("/src/storage/StorageClient.ts"); const digest = async (data: ArrayBuffer): Promise => Array.from( new Uint8Array(await crypto.subtle.digest("SHA-256", data)), (byte) => byte.toString(16).padStart(2, "0"), ).join(""); const revisionHash = async (binding: { graphId: string; graphHash: string; sourceBlendSha256: string; sourceRevision: number; inputHash: string; blenderVersion: string; frameStart: number; frameEnd: number; }): Promise => digest(new TextEncoder().encode(JSON.stringify([ "blender-web-simulation-cache-revision-v2", binding.graphId, binding.graphHash, binding.sourceBlendSha256, String(binding.sourceRevision), binding.inputHash, binding.blenderVersion, String(binding.frameStart), String(binding.frameEnd), ])).buffer); const errorCode = async (operation: () => Promise): Promise => { try { await operation(); return ""; } catch (error) { return String((error as Error & { code?: string }).code ?? ""); } }; const projectId = `m10-05-${Date.now()}`; const sourceOne = Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44, 1]).buffer; const sourceTwo = Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44, 2]).buffer; const payload = Uint8Array.from([11, 12, 13, 14]).buffer; const frameHash = await digest(payload); const common = { graphId: "node-group:M10SimulationIdentity", graphHash: await digest(Uint8Array.from([21]).buffer), inputHash: await digest(Uint8Array.from([22]).buffer), blenderVersion: "5.2.0", frameStart: 1, frameEnd: 1, }; const makeManifest = async (source: ArrayBuffer, sourceRevision: number) => { const binding = { ...common, sourceBlendSha256: await digest(source), sourceRevision }; return { schemaVersion: 2 as const, ...binding, revisionHash: await revisionHash(binding), cacheSha256: frameHash, byteLength: payload.byteLength, frames: [{ frame: 1, byteOffset: 0, byteLength: payload.byteLength, sha256: frameHash }], }; }; const firstManifest = await makeManifest(sourceOne, 7); const first = new StorageClient(); await first.saveProject(projectId, 7, sourceOne.slice(0)); const storedOne = await first.putSimulationCache(projectId, firstManifest, payload.slice(0)); const forgedGraphCode = await errorCode(() => first.putSimulationCache( projectId, { ...firstManifest, graphHash: "0".repeat(64) }, payload.slice(0), )); const forgedSourceBinding = { ...firstManifest, sourceBlendSha256: "1".repeat(64) }; const forgedSourceManifest = { ...forgedSourceBinding, revisionHash: await revisionHash(forgedSourceBinding), }; const forgedSourceCode = await errorCode(() => first.putSimulationCache( projectId, forgedSourceManifest, payload.slice(0), )); first.terminate(); const restarted = new StorageClient(); await restarted.prepareSimulationCachePlayback(projectId, storedOne.cacheKey); const recovered = await restarted.readSimulationCacheFrame(projectId, storedOne.cacheKey, 1); const listedBefore = await restarted.listSimulationCaches(projectId); await restarted.saveProject(projectId, 8, sourceTwo.slice(0)); const staleReadCode = await errorCode(() => restarted.readSimulationCache(projectId, storedOne.cacheKey)); const listedAfter = await restarted.listSimulationCaches(projectId); const stalePutCode = await errorCode(() => restarted.putSimulationCache( projectId, firstManifest, payload.slice(0), )); const secondManifest = await makeManifest(sourceTwo, 8); const storedTwo = await restarted.putSimulationCache(projectId, secondManifest, payload.slice(0)); const listedCurrent = await restarted.listSimulationCaches(projectId); restarted.terminate(); return { firstKey: storedOne.cacheKey, secondKey: storedTwo.cacheKey, recoveredBytes: Array.from(new Uint8Array(recovered.data)), listedBefore: listedBefore.caches.map((cache) => cache.cacheKey), listedAfter: listedAfter.caches.map((cache) => cache.cacheKey), listedCurrent: listedCurrent.caches.map((cache) => cache.cacheKey), forgedGraphCode, forgedSourceCode, staleReadCode, stalePutCode, sourceRevision: storedTwo.manifest.sourceRevision, }; }); expect(result.firstKey).toMatch(/^sim2-[a-f0-9]{64}$/); expect(result.secondKey).toMatch(/^sim2-[a-f0-9]{64}$/); expect(result.secondKey).not.toBe(result.firstKey); expect(result.recoveredBytes).toEqual([11, 12, 13, 14]); expect(result.listedBefore).toEqual([result.firstKey]); expect(result.listedAfter).toEqual([]); expect(result.listedCurrent).toEqual([result.secondKey]); expect(result.forgedGraphCode).toBe("SIMULATION_CACHE_REVISION_MISMATCH"); expect(result.forgedSourceCode).toBe("SIMULATION_CACHE_HASH_MISMATCH"); expect(result.staleReadCode).toBe("SIMULATION_CACHE_REVISION_MISMATCH"); expect(result.stalePutCode).toBe("SIMULATION_CACHE_REVISION_MISMATCH"); expect(result.sourceRevision).toBe(8); });