Advance M8-M11 parity workflows
This commit is contained in:
115
web/tests/e2e/simulation-cache-identity.spec.ts
Normal file
115
web/tests/e2e/simulation-cache-identity.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user