Files
workinf_Blender_Wasm/web/tests/unit/library-archive-recovery.test.mjs
mes123456 380cbed4ff
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
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

164 lines
7.5 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-recovery-unit-"));
for (const sourceName of ["archive-extraction-transaction.ts", "archive-extraction-recovery.ts"]) {
const sourcePath = path.join(root, "web/protocol", sourceName);
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(
path.join(temporary, sourceName.replace(".ts", ".mjs")),
transpiled.outputText.replaceAll('from "./archive-extraction-transaction"', 'from "./archive-extraction-transaction.mjs"'),
);
}
const extraction = await import(pathToFileURL(path.join(temporary, "archive-extraction-recovery.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04I/manifest.json"), "utf8"));
const fileSha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
const bytesSha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
class FaultingDirectoryStorage {
constructor(directory, committed) {
this.directory = directory;
this.committed = { ...committed };
this.fault = null;
this.partialBytes = 0;
fs.mkdirSync(path.join(directory, "committed"), { recursive: true });
fs.writeFileSync(path.join(directory, "committed", "project.blend"), Buffer.from("old-project"));
}
async readCommitted() { return { ...this.committed }; }
async createStaging(transactionId) {
fs.mkdirSync(path.join(this.directory, "staging", transactionId), { recursive: true });
}
async writeStaging(transactionId, entryPath, bytes) {
const target = path.join(this.directory, "staging", transactionId, entryPath);
fs.mkdirSync(path.dirname(target), { recursive: true });
if (this.fault) {
const partial = bytes.subarray(0, Math.max(1, Math.floor(bytes.byteLength / 2)));
fs.writeFileSync(target, partial);
this.partialBytes += partial.byteLength;
const fault = this.fault;
this.fault = null;
throw fault;
}
fs.writeFileSync(target, bytes);
}
async countStagingEntries(transactionId) {
const staging = path.join(this.directory, "staging", transactionId);
if (!fs.existsSync(staging)) return 0;
const visit = (directory) => fs.readdirSync(directory, { withFileTypes: true }).reduce(
(count, entry) => count + (entry.isDirectory() ? visit(path.join(directory, entry.name)) : 1), 0,
);
return visit(staging);
}
async discardStaging(transactionId) {
const count = await this.countStagingEntries(transactionId);
fs.rmSync(path.join(this.directory, "staging", transactionId), { recursive: true, force: true });
return count;
}
async commitStaging(transactionId, expected, candidate) {
assert.deepEqual(this.committed, expected);
const source = path.join(this.directory, "staging", transactionId, "project.blend");
const target = path.join(this.directory, "committed", "project.blend");
fs.renameSync(source, target);
await this.discardStaging(transactionId);
this.committed = { ...candidate };
return { ...this.committed };
}
}
const oldBytes = Buffer.from("old-project");
const largeBytes = Buffer.alloc(4096, 0x51);
const smallBytes = Buffer.from("small-project");
const committed = { projectId: "project:m12-04i", revision: 9, sha256: bytesSha256(oldBytes) };
function request(transactionId, base, bytes) {
return {
schemaVersion: 1,
transactionId,
archiveId: `archive:${transactionId}`,
committed: base,
candidate: { projectId: base.projectId, revision: base.revision + 1, sha256: bytesSha256(bytes) },
entries: [{ path: "project.blend", uncompressedBytes: bytes.byteLength, sha256: bytesSha256(bytes) }],
};
}
function createStorage(name) {
const directory = path.join(temporary, name);
fs.mkdirSync(directory, { recursive: true });
return new FaultingDirectoryStorage(directory, committed);
}
async function faultThenRecover(name, fault, expectedCode) {
const storage = createStorage(name);
storage.fault = fault;
const failedRequest = request(`transaction:${name}:large`, committed, largeBytes);
await assert.rejects(
extraction.runArchiveExtractionTransaction(failedRequest, storage, async () => largeBytes, new AbortController().signal),
{ code: expectedCode },
);
assert.ok(storage.partialBytes > 0);
assert.equal(await storage.countStagingEntries(failedRequest.transactionId), 0);
assert.deepEqual(await storage.readCommitted(), committed);
assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "old-project");
const recoveryRequest = request(`transaction:${name}:small`, committed, smallBytes);
const receipt = await extraction.runArchiveExtractionTransaction(
recoveryRequest,
storage,
async () => smallBytes,
new AbortController().signal,
);
assert.deepEqual(receipt, {
status: "COMMITTED",
transactionId: recoveryRequest.transactionId,
committed: recoveryRequest.candidate,
stagingEntriesAfter: 0,
});
assert.equal(await storage.countStagingEntries(recoveryRequest.transactionId), 0);
assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "small-project");
}
test("M12-04I binds quota/OOM cleanup and recovery evidence", () => {
assert.equal(manifest.task, "M12-04I");
assert.equal(manifest.parentTask, "M12-04H");
assert.equal(manifest.nextTask, "M12-04J");
assert.deepEqual(manifest.assertions.faultCodes, ["STORAGE_QUOTA", "WASM_OUT_OF_MEMORY"]);
for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(artifact.path), artifact.sha256, artifact.path);
});
test("M12-04I releases partial staging after quota and accepts a small archive in the same storage", async () => {
await faultThenRecover("quota", new DOMException("quota exhausted", "QuotaExceededError"), "STORAGE_QUOTA");
});
test("M12-04I releases partial staging after OOM and accepts a small archive in the same storage", async () => {
await faultThenRecover("oom", Object.assign(new Error("deterministic allocation failure"), { code: "WASM_OUT_OF_MEMORY" }), "WASM_OUT_OF_MEMORY");
});
test("M12-04I maps only declared resource faults", () => {
assert.equal(extraction.archiveExtractionResourceFaultCode(new DOMException("full", "QuotaExceededError")), "STORAGE_QUOTA");
assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { code: "STORAGE_QUOTA" })), "STORAGE_QUOTA");
assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { code: "WASM_OUT_OF_MEMORY" })), "WASM_OUT_OF_MEMORY");
assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { name: "OutOfMemoryError" })), "WASM_OUT_OF_MEMORY");
assert.equal(extraction.archiveExtractionResourceFaultCode(new RangeError("array length is invalid")), undefined);
assert.equal(extraction.archiveExtractionResourceFaultCode(new Error("ordinary IO failure")), undefined);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));