| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | import { spawnSync } from "node:child_process"; |
| 4 | import { mkdtempSync, writeFileSync, rmSync, readFileSync } from "node:fs"; |
| 5 | import { tmpdir } from "node:os"; |
| 6 | import { join } from "node:path"; |
| 7 | import { fileURLToPath } from "node:url"; |
| 8 | import { createCoreLedger, createSiteLedger, mergeLedgers, npmPackageNames, ownsPublicSite } from "./release-publication-ledger.mjs"; |
| 9 | |
| 10 | const sha = "a".repeat(40); |
| 11 | const release = { isDraft: false, isPrerelease: false, assets: [{ name: "asset.zip", size: 42, digest: "sha256:abc" }] }; |
| 12 | const packages = latest => npmPackageNames.map(name => ({ |
| 13 | name, |
| 14 | version: "1.2.3", |
| 15 | reasonixCandidateSha: sha, |
| 16 | gitHead: sha, |
| 17 | integrity: "sha512-example", |
| 18 | latest, |
| 19 | })); |
| 20 | |
| 21 | test("records immutable files and exact current pointers", () => { |
| 22 | const core = createCoreLedger({ |
| 23 | version: "1.2.3", sourceSHA: sha, operation: "publish", |
| 24 | cliRelease: release, desktopRelease: release, npmPackages: packages("1.2.3"), |
| 25 | }); |
| 26 | assert.equal(core.surfaces.tags.items.length, 3); |
| 27 | assert.ok(core.surfaces.npm.packages.every(item => item.pointerState === "public-entry-updated")); |
| 28 | assert.equal(core.surfaces.cli.assets[0].digest, "sha256:abc"); |
| 29 | }); |
| 30 | |
| 31 | test("recovery preserves newer public pointers", () => { |
| 32 | const core = createCoreLedger({ |
| 33 | version: "1.2.3", sourceSHA: sha, operation: "recover", |
| 34 | cliRelease: release, desktopRelease: release, npmPackages: packages("1.3.0"), |
| 35 | }); |
| 36 | assert.ok(core.surfaces.npm.packages.every(item => item.pointerState === "newer-entry-preserved")); |
| 37 | assert.throws(() => createCoreLedger({ |
| 38 | version: "1.2.3", sourceSHA: sha, operation: "publish", |
| 39 | cliRelease: release, desktopRelease: release, npmPackages: packages("1.3.0"), |
| 40 | }), /npm latest is inconsistent/); |
| 41 | }); |
| 42 | |
| 43 | test("site evidence merges only for the same candidate", () => { |
| 44 | const core = createCoreLedger({ |
| 45 | version: "1.2.3", sourceSHA: sha, operation: "publish", |
| 46 | cliRelease: release, desktopRelease: release, npmPackages: packages("1.2.3"), |
| 47 | }); |
| 48 | const site = createSiteLedger({ |
| 49 | version: "1.2.3", sourceSHA: sha, operation: "publish", manifest: { version: "v1.2.3" }, |
| 50 | }); |
| 51 | assert.equal(mergeLedgers(core, site).surfaces.homepage.state, "public-entry-updated"); |
| 52 | assert.throws(() => mergeLedgers(core, { ...site, sourceSHA: "b".repeat(40) }), /one release/); |
| 53 | }); |
| 54 | |
| 55 | test("site ownership requires an exact release or a proven newer recovery pointer", () => { |
| 56 | for (const operation of ["publish", "recover"]) { |
| 57 | assert.equal(ownsPublicSite("1.2.3", operation, { version: "v1.2.3" }), true); |
| 58 | for (const version of [undefined, "", "v1.2.2", "1.2.3", "v1.2.3-beta.1"]) { |
| 59 | assert.throws(() => ownsPublicSite("1.2.3", operation, { version })); |
| 60 | } |
| 61 | } |
| 62 | assert.equal(ownsPublicSite("1.2.3", "recover", { version: "v1.10.0" }), false); |
| 63 | assert.throws(() => ownsPublicSite("1.2.3", "publish", { version: "v1.10.0" })); |
| 64 | assert.throws(() => ownsPublicSite("1.2.3", "unknown", { version: "v1.2.3" })); |
| 65 | }); |
| 66 | |
| 67 | test("actual site observation fails closed on HTTP errors and malformed or stale responses", () => { |
| 68 | const directory = mkdtempSync(join(tmpdir(), "release-site-observation-")); |
| 69 | try { |
| 70 | writeFileSync(join(directory, "curl"), '#!/bin/sh\nprintf "%s" "$RESPONSE_BODY"\nexit "$RESPONSE_STATUS"\n', { mode: 0o755 }); |
| 71 | const helper = fileURLToPath(new URL("./observe-release-site.sh", import.meta.url)); |
| 72 | for (const [status, body, expected] of [ |
| 73 | [22, "", null], [22, '{"version":"v1.3.0"}', null], |
| 74 | [0, "", null], [0, "<html>Forbidden</html>", null], |
| 75 | [0, '{"version":"v1.2.2"}', null], |
| 76 | [0, '{"version":"v1.2.3"}', "true"], |
| 77 | [0, '{"version":"v1.10.0"}', "false"], |
| 78 | ]) { |
| 79 | const result = spawnSync("bash", [helper, "1.2.3", "recover"], { |
| 80 | encoding: "utf8", env: { ...process.env, PATH: `${directory}:${process.env.PATH}`, RESPONSE_STATUS: String(status), RESPONSE_BODY: body }, |
| 81 | }); |
| 82 | if (expected === null) { |
| 83 | assert.notEqual(result.status, 0, `must reject HTTP ${status}: ${body}`); |
| 84 | assert.equal(result.stdout, ""); |
| 85 | } else { |
| 86 | assert.equal(result.status, 0, result.stderr); |
| 87 | assert.equal(result.stdout.trim(), expected); |
| 88 | } |
| 89 | } |
| 90 | for (const file of ["../.github/workflows/release-promote.yml", "./verify-stable-release-artifacts.sh"]) { |
| 91 | assert.match(readFileSync(new URL(file, import.meta.url), "utf8"), /observe-release-site\.sh/); |
| 92 | } |
| 93 | } finally { |
| 94 | rmSync(directory, { recursive: true, force: true }); |
| 95 | } |
| 96 | }); |
| 97 |