import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { pthreadSidecar, artifactRevision, writeManifestIfChanged, missingArtifactPair, copyArtifactAtomically } from "./artifact-assets.mjs"; test("publishing a WASM leaves the old complete file visible until copying finishes", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-atomic-")); try { const source = path.join(dir, "source.wasm"), target = path.join(dir, "public.wasm"); const previous = Buffer.from([0, 97, 115, 109, 1, 0, 0, 0]); const next = Buffer.concat([previous, Buffer.from([0, 2, 0, 1])]); fs.writeFileSync(source, next); fs.writeFileSync(target, previous); copyArtifactAtomically(source, target, (from, temporary) => { fs.writeFileSync(temporary, "partial"); assert.deepEqual(fs.readFileSync(target), previous); fs.copyFileSync(from, temporary); }); assert.deepEqual(fs.readFileSync(target), next); assert.throws(() => copyArtifactAtomically(source, target, (_from, temporary) => { fs.writeFileSync(temporary, "partial"); throw Error("interrupted copy"); }), /interrupted copy/); assert.deepEqual(fs.readFileSync(target), next); fs.writeFileSync(source, next.subarray(0, next.length - 1)); assert.throws(() => copyArtifactAtomically(source, target), /Incomplete or invalid WASM/); assert.deepEqual(fs.readFileSync(target), next); fs.writeFileSync(source, next); assert.throws(() => copyArtifactAtomically(source, target, (from, temporary) => { fs.copyFileSync(from, temporary); fs.appendFileSync(from, Buffer.from([0])); }), /changed while copying/); assert.deepEqual(fs.readFileSync(target), next); assert.deepEqual(fs.readdirSync(dir).sort(), ["public.wasm", "source.wasm"]); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); test("an unfinished native link cannot publish a new WASM with an old loader", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-pair-")); try { const wasm = path.join(dir, "FreeCADWebDocumentProbe.wasm"); const runtime = path.join(dir, "FreeCADWebDocumentProbe.js"); fs.writeFileSync(wasm, "new wasm from incomplete link"); assert.deepEqual(missingArtifactPair(wasm), [runtime]); fs.writeFileSync(runtime, "matching runtime"); assert.deepEqual(missingArtifactPair(wasm), []); assert.deepEqual(missingArtifactPair(runtime), []); fs.unlinkSync(wasm); assert.deepEqual(missingArtifactPair(runtime), [wasm]); assert.deepEqual(missingArtifactPair(path.join(dir, "freecad-sample.step")), []); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); test("thread worker follows its runtime from the same build directory", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-assets-")); try { const runtime = path.join(dir, "Probe.js"); const destination = path.join(dir, "public/Probe.js"); assert.equal(pthreadSidecar(runtime, destination), null); fs.writeFileSync(path.join(dir, "Probe.worker.js"), "thread worker"); assert.deepEqual(pthreadSidecar(runtime, destination), [path.join(dir, "Probe.worker.js"), path.join(dir, "public/Probe.worker.js")]); assert.equal(pthreadSidecar(path.join(dir, "Probe.worker.js"), destination), null); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); test("runtime, data and worker changes invalidate revisions independently of WASM", () => { const hashes = { runtime: "js1", wasm: "wasm", data: "data1", pthreadWorker: "worker1" }; const revision = artifactRevision(hashes); for (const key of Object.keys(hashes)) assert.notEqual(artifactRevision({ ...hashes, [key]: "changed" }), revision); assert.equal(artifactRevision({ ...hashes }), revision); }); test("unchanged manifest preserves contents and mtime across synchronization", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-manifest-")); try { const file = path.join(dir, "manifest.json"); const value = { schema: "manifest/v1", generatedAt: "first", modules: { Draft: { revision: "one" } } }; assert.equal(writeManifestIfChanged(file, value), true); const before = fs.statSync(file).mtimeNs ?? fs.statSync(file).mtimeMs; assert.equal(writeManifestIfChanged(file, { ...value, generatedAt: "second" }), false); assert.equal(fs.statSync(file).mtimeNs ?? fs.statSync(file).mtimeMs, before); assert.equal(JSON.parse(fs.readFileSync(file, "utf8")).generatedAt, "first"); assert.equal(writeManifestIfChanged(file, { ...value, modules: { Draft: { revision: "two" } } }), true); } finally { fs.rmSync(dir, { recursive: true, force: true }); } });