Advance M8-M11 parity workflows
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-17 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -0,0 +1,211 @@
import { expect, test } from "@playwright/test";
test("M10-06 gates playback on verification and enforces cancel, LRU, restart and corruption quarantine", async ({ page }) => {
test.setTimeout(120_000);
await page.goto("/");
const result = await page.evaluate(async () => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const { BrowserTransformCachePlaybackSession } = await import("/src/simulation/BrowserTransformCachePlayback.ts");
const { upgradeStorageSchema } = await import("/src/storage/migrations.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 errorCode = async (operation: () => Promise<unknown>): Promise<string> => {
try { await operation(); return ""; }
catch (error) {
const code = (error as Error & { code?: unknown }).code;
return typeof code === "string" ? code : (error as Error).name;
}
};
const projectId = `m10-06-${Date.now()}`;
const migrationDatabase = `m10-06-migration-${Date.now()}`;
await new Promise<void>((resolve, reject) => {
const request = indexedDB.open(migrationDatabase, 6);
request.onupgradeneeded = () => {
request.result.createObjectStore("migration", { keyPath: "id" });
request.result.createObjectStore("simulation_manifest", { keyPath: "id" });
};
request.onsuccess = () => { request.result.close(); resolve(); };
request.onerror = () => reject(request.error);
});
const migration = await new Promise<{ version: number; stores: string[]; record?: { version: number } }>((resolve, reject) => {
const request = indexedDB.open(migrationDatabase, 7);
request.onupgradeneeded = (event) => upgradeStorageSchema(request.result, request.transaction!, (event as IDBVersionChangeEvent).oldVersion);
request.onsuccess = () => {
const database = request.result;
const transaction = database.transaction("migration", "readonly");
const record = transaction.objectStore("migration").get("schema-7");
record.onsuccess = () => resolve({ version: database.version, stores: [...database.objectStoreNames], record: record.result as { version: number } | undefined });
record.onerror = () => reject(record.error);
transaction.oncomplete = () => database.close();
};
request.onerror = () => reject(request.error);
});
indexedDB.deleteDatabase(migrationDatabase);
const sourceBlend = Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44, 6]).buffer;
const sourceBlendSha256 = await digest(sourceBlend);
const makeManifest = async (name: string, payload: ArrayBuffer, frameCount = 1) => {
const frameBytes = payload.byteLength / frameCount;
const binding = {
graphId: `node-group:${name}`,
graphHash: await digest(new TextEncoder().encode(`graph:${name}`).buffer),
sourceBlendSha256,
sourceRevision: 6,
inputHash: await digest(new TextEncoder().encode(`input:${name}`).buffer),
blenderVersion: "5.2.0",
frameStart: 1,
frameEnd: frameCount,
};
const revisionHash = await 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 frames = await Promise.all(Array.from({ length: frameCount }, async (_, index) => ({
frame: index + 1,
byteOffset: index * frameBytes,
byteLength: frameBytes,
sha256: await digest(payload.slice(index * frameBytes, (index + 1) * frameBytes)),
})));
return {
schemaVersion: 2 as const,
...binding,
revisionHash,
cacheSha256: await digest(payload),
byteLength: payload.byteLength,
frames,
};
};
const first = new StorageClient();
await first.saveProject(projectId, 6, sourceBlend.slice(0));
const payloadA = Uint8Array.from([1, 2, 3, 4]).buffer;
const manifestA = await makeManifest("lru-a", payloadA);
const storedA = await first.putSimulationCache(projectId, manifestA, payloadA.slice(0));
first.terminate();
const restarted = new StorageClient();
const notReadyAfterRestart = await errorCode(() => restarted.readSimulationCacheFrame(projectId, storedA.cacheKey, 1));
const prepared = await restarted.prepareSimulationCachePlayback(projectId, storedA.cacheKey);
const recovered = await restarted.readSimulationCacheFrame(projectId, storedA.cacheKey, 1);
let cancelledPublishedFrames = 0;
const playback = new BrowserTransformCachePlaybackSession({
schemaVersion: 1 as const,
revision: 6,
sceneId: "scene:M10-06",
source: { kind: "mock" as const },
coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 },
activeObjectId: null,
frame: { current: 1, start: 1, end: 1 },
nodes: [], meshes: [], materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [],
}, {
frameStart: 1,
frameEnd: 1,
readFrame: async (frame, signal) => (await restarted.readSimulationCacheFrame(projectId, storedA.cacheKey, frame, signal)).data,
}, () => { cancelledPublishedFrames += 1; });
const cancellingPlayback = playback.play();
playback.cancel();
const playbackCancellation = await cancellingPlayback;
const pendingAfterPlaybackCancel = restarted.getPendingRequestCount();
await new Promise((resolve) => setTimeout(resolve, 5));
const payloadB = Uint8Array.from([5, 6, 7, 8]).buffer;
const manifestB = await makeManifest("lru-b", payloadB);
const storedB = await restarted.putSimulationCache(projectId, manifestB, payloadB.slice(0));
await new Promise((resolve) => setTimeout(resolve, 5));
const payloadC = Uint8Array.from([9, 10, 11, 12]).buffer;
const manifestC = await makeManifest("lru-c", payloadC);
const storedC = await restarted.putSimulationCache(projectId, manifestC, payloadC.slice(0));
const activePrune = await restarted.pruneSimulationCaches(projectId, 4);
const afterActivePrune = await restarted.listSimulationCaches(projectId);
await restarted.releaseSimulationCachePlayback(projectId, storedA.cacheKey);
const releasedCode = await errorCode(() => restarted.readSimulationCacheFrame(projectId, storedA.cacheKey, 1));
const finalPrune = await restarted.pruneSimulationCaches(projectId, 0);
const cancelledPayload = new ArrayBuffer(256 * 32);
const cancelledBytes = new Uint8Array(cancelledPayload);
for (let index = 0; index < cancelledBytes.length; index += 1) cancelledBytes[index] = index % 251;
const cancelledManifest = await makeManifest("cancelled", cancelledPayload, 256);
const controller = new AbortController();
const cancelledWrite = restarted.putSimulationCache(projectId, cancelledManifest, cancelledPayload, controller.signal);
controller.abort();
const cancelledCode = await errorCode(() => cancelledWrite);
const afterCancel = await restarted.listSimulationCaches(projectId);
const assetsAfterCancel = await restarted.listAssets(projectId);
const corruptPayload = Uint8Array.from([31, 32, 33, 34]).buffer;
const corruptManifest = await makeManifest("corrupt", corruptPayload);
const corruptStored = await restarted.putSimulationCache(projectId, corruptManifest, corruptPayload.slice(0));
restarted.terminate();
const pathSegments = corruptStored.path.split("/");
const fileName = pathSegments.pop()!;
let directory = await navigator.storage.getDirectory();
for (const segment of pathSegments) directory = await directory.getDirectoryHandle(segment);
const handle = await directory.getFileHandle(fileName);
const writable = await handle.createWritable();
await writable.write(Uint8Array.from([99, 98, 97, 96]));
await writable.close();
const quarantineReader = new StorageClient();
const corruptNotReady = await errorCode(() => quarantineReader.readSimulationCacheFrame(projectId, corruptStored.cacheKey, 1));
const corruptionCode = await errorCode(() => quarantineReader.prepareSimulationCachePlayback(projectId, corruptStored.cacheKey));
const afterCorruption = await quarantineReader.listSimulationCaches(projectId);
const info = await quarantineReader.info();
const pending = quarantineReader.getPendingRequestCount();
quarantineReader.terminate();
return {
notReadyAfterRestart,
verifiedAt: prepared.verifiedAt,
recovered: Array.from(new Uint8Array(recovered.data)),
playbackCancellation,
cancelledPublishedFrames,
pendingAfterPlaybackCancel,
activePrune,
activeKeys: afterActivePrune.caches.map((cache) => cache.cacheKey),
expectedActiveKey: storedA.cacheKey,
evictedKeys: [storedB.cacheKey, storedC.cacheKey].sort(),
releasedCode,
finalPrune,
cancelledCode,
cancelledPublished: afterCancel.caches.some((cache) => cache.cacheKey === `sim2-${cancelledManifest.revisionHash}`),
cancelledAssetPresent: assetsAfterCancel.assets.some((asset) => asset.sha256 === cancelledManifest.cacheSha256),
corruptNotReady,
corruptionCode,
cachesAfterCorruption: afterCorruption.caches.length,
quarantined: afterCorruption.quarantined,
issues: afterCorruption.issues,
schemaVersion: info.schemaVersion,
stores: info.stores,
pending,
migration,
};
});
expect(result.notReadyAfterRestart).toBe("SIMULATION_CACHE_NOT_READY");
expect(result.verifiedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(result.recovered).toEqual([1, 2, 3, 4]);
expect(result.playbackCancellation).toEqual({ status: "CANCELLED", appliedFrames: 0, lastFrame: null });
expect(result.cancelledPublishedFrames).toBe(0);
expect(result.pendingAfterPlaybackCancel).toBe(0);
expect(result.activePrune).toMatchObject({ beforeBytes: 12, remainingBytes: 4, removedBytes: 8, removed: 2, budgetSatisfied: true });
expect(result.activePrune.cacheKeys.sort()).toEqual(result.evictedKeys);
expect(result.activePrune.protectedCacheKeys).toContain(result.expectedActiveKey);
expect(result.activeKeys).toEqual([result.expectedActiveKey]);
expect(result.releasedCode).toBe("SIMULATION_CACHE_NOT_READY");
expect(result.finalPrune).toMatchObject({ beforeBytes: 4, remainingBytes: 0, removedBytes: 4, removed: 1, budgetSatisfied: true });
expect(result.cancelledCode).toBe("AbortError");
expect(result.cancelledPublished).toBe(false);
expect(result.cancelledAssetPresent).toBe(false);
expect(result.corruptNotReady).toBe("SIMULATION_CACHE_NOT_READY");
expect(result.corruptionCode).toBe("SIMULATION_CACHE_HASH_MISMATCH");
expect(result.cachesAfterCorruption).toBe(0);
expect(result.quarantined).toBe(1);
expect(result.issues).toEqual([expect.objectContaining({ code: "SIMULATION_CACHE_HASH_MISMATCH" })]);
expect(result.schemaVersion).toBe(7);
expect(result.stores).toContain("simulation_quarantine");
expect(result.pending).toBe(0);
expect(result.migration).toMatchObject({ version: 7, record: { version: 7 } });
expect(result.migration.stores).toContain("simulation_quarantine");
});