116 lines
5.4 KiB
TypeScript
116 lines
5.4 KiB
TypeScript
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<string> => 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<string> => 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<unknown>): Promise<string> => {
|
|
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);
|
|
});
|