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(), "asset-preview-identity-unit-")); const sourcePath = path.join(root, "web/protocol/asset-preview.ts"); 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, "asset-preview.mjs"), transpiled.outputText); const preview = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs"))); const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/manifest.json"), "utf8")); const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8")); const sourceBytes = fs.readFileSync(path.join(root, manifest.artifacts.source.path)); const contentBytes = fs.readFileSync(path.join(root, manifest.artifacts.content.path)); const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); const stableJSON = (value) => Array.isArray(value) ? `[${value.map(stableJSON).join(",")}]` : value && typeof value === "object" ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}` : JSON.stringify(value); test("M12-02B binds source, content, generator, protocol, and settings artifacts", () => { assert.equal(manifest.task, "M12-02B"); assert.equal(manifest.enablingTask, true); assert.equal(manifest.parityStateChange, false); assert.equal(manifest.nextTask, "M12-02C"); for (const artifact of Object.values(manifest.artifacts)) { assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); } assert.equal(sha256(stableJSON(manifest.generatorSettings)), manifest.generatorSettingsSha256); assert.equal(manifest.generatorSettingsSha256, golden.generator.settingsSha256); }); test("M12-02B creates and parses the canonical preview identity", async () => { const { identitySha256, ...base } = golden; assert.deepEqual(await preview.createAssetPreviewIdentity(base), golden); assert.deepEqual(await preview.parseAssetPreviewIdentity(golden), golden); assert.equal(identitySha256, await preview.computeAssetPreviewIdentity(base)); assert.notEqual(golden.source.sha256, golden.content.sha256); assert.deepEqual(await preview.verifyAssetPreviewIdentity( golden, sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength), contentBytes.buffer.slice(contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength), golden.generator, ), golden); }); test("M12-02B rejects independent source, content, generator, dimensions, and unknown-field drift", async () => { const source = sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength); const content = contentBytes.buffer.slice(contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength); const changedSource = source.slice(0); new Uint8Array(changedSource)[0] ^= 1; const changedContent = content.slice(0); new Uint8Array(changedContent)[0] ^= 1; await assert.rejects(preview.verifyAssetPreviewIdentity(golden, changedSource, content, golden.generator), { code: "ASSET_SOURCE_HASH_MISMATCH" }); await assert.rejects(preview.verifyAssetPreviewIdentity(golden, source, changedContent, golden.generator), { code: "ASSET_SOURCE_HASH_MISMATCH" }); await assert.rejects(preview.verifyAssetPreviewIdentity(golden, source, content, { ...golden.generator, settingsSha256: "f".repeat(64) }), { code: "ASSET_PREVIEW_IDENTITY_MISMATCH" }); await assert.rejects(preview.parseAssetPreviewIdentity({ ...golden, content: { ...golden.content, width: 9 } }), { code: "ASSET_PREVIEW_IDENTITY_MISMATCH" }); await assert.rejects(preview.parseAssetPreviewIdentity({ ...golden, future: true }), { code: "ASSET_MANIFEST_INVALID" }); }); test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));