import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const builder = join(scriptDir, "build-ops-agents-reboot-resilience-artifact.mjs"); const basename = "nodedc-ops-agents-reboot-resilience-20260809-001"; test("builds a deterministic, minimal Ops Agents application overlay", async () => { const firstRoot = await mkdtemp(join(tmpdir(), "ops-agents-artifact-first-")); const secondRoot = await mkdtemp(join(tmpdir(), "ops-agents-artifact-second-")); try { const first = runBuilder(firstRoot); const second = runBuilder(secondRoot); const firstArtifact = join(firstRoot, `${basename}.tgz`); const secondArtifact = join(secondRoot, `${basename}.tgz`); assert.equal(first.artifactSha256, second.artifactSha256); assert.equal(first.artifactSha256, sha256(await readFile(firstArtifact))); assert.deepEqual(first.files, [ "docker-compose.synology.yml", "src/db/wait-for-database.test.ts", "src/db/wait-for-database.ts", "src/scripts/migrate.ts", ]); assert.equal(first.runnerChanged, false); assert.equal(first.databaseVolumeSelected, false); const members = new Set(runTar(["-tzf", firstArtifact]).trim().split("\n")); assert.deepEqual(members, new Set([ "manifest.env", "files.txt", "payload/", "payload/docker-compose.synology.yml", "payload/src/", "payload/src/db/", "payload/src/db/wait-for-database.test.ts", "payload/src/db/wait-for-database.ts", "payload/src/scripts/", "payload/src/scripts/migrate.ts", ])); assert.equal([...members].some((member) => member.includes("._")), false); assert.equal( runTar(["-xOzf", firstArtifact, "manifest.env"]), "id=ops-agents-reboot-resilience-20260809-001\ncomponent=ops-agents\ntype=app-overlay\n", ); assert.equal( runTar(["-xOzf", firstArtifact, "files.txt"]), `${first.files.join("\n")}\n`, ); const predecessors = await readFile(join(firstRoot, `${basename}.predecessor.sha256`), "utf8"); assert.match(predecessors, /^e78d37368c3d255f257086773ba908e44aae2e475661b80ee17f98c3d55d178f .*docker-compose\.synology\.yml$/m); assert.match(predecessors, /^8396a42deded31f1fe2381f772838a33b05bfd389075ebd34a5f546593f08769 .*src\/scripts\/migrate\.ts$/m); const newPaths = await readFile(join(firstRoot, `${basename}.new-paths`), "utf8"); assert.match(newPaths, /src\/db\/wait-for-database\.ts/); assert.match(newPaths, /src\/db\/wait-for-database\.test\.ts/); } finally { await rm(firstRoot, { recursive: true, force: true }); await rm(secondRoot, { recursive: true, force: true }); } }); function runBuilder(outputRoot) { const result = spawnSync(process.execPath, [builder], { encoding: "utf8", env: { ...process.env, NODEDC_DEPLOY_ARTIFACT_DIR: outputRoot }, }); assert.equal(result.status, 0, result.stderr || result.stdout); return JSON.parse(result.stdout); } function runTar(args) { const result = spawnSync("tar", args, { encoding: "utf8" }); assert.equal(result.status, 0, result.stderr || result.stdout); return result.stdout; } function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }