| 1 | const assert = require("node:assert/strict"); |
| 2 | const { execFileSync } = require("node:child_process"); |
| 3 | const fs = require("node:fs"); |
| 4 | const os = require("node:os"); |
| 5 | const path = require("node:path"); |
| 6 | const test = require("node:test"); |
| 7 | |
| 8 | const pkg = require("../package.json"); |
| 9 | const { |
| 10 | allReleaseAssetNames, |
| 11 | BUNDLE_ASSET_NAMES, |
| 12 | BUNDLE_CHECKSUM_MANIFEST, |
| 13 | CHECKSUM_MANIFEST, |
| 14 | checksummedReleaseAssetNames, |
| 15 | } = require("../scripts/artifacts"); |
| 16 | const { |
| 17 | assertChecksumManifestIncludes, |
| 18 | assertPackageVersionMatchesBinaryVersion, |
| 19 | assertReleaseAssetsFresh, |
| 20 | parseChecksumManifest, |
| 21 | } = require("../scripts/verify-release-assets"); |
| 22 | |
| 23 | test("parseChecksumManifest accepts GNU and BSD filename forms", () => { |
| 24 | const manifest = parseChecksumManifest( |
| 25 | [ |
| 26 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa codewhale-linux-x64", |
| 27 | "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb *codewhale-windows-x64.exe", |
| 28 | ].join("\n"), |
| 29 | ); |
| 30 | |
| 31 | assert.equal(manifest.get("codewhale-linux-x64"), "a".repeat(64)); |
| 32 | assert.equal(manifest.get("codewhale-windows-x64.exe"), "b".repeat(64)); |
| 33 | }); |
| 34 | |
| 35 | test("parseChecksumManifest rejects malformed checksum rows", () => { |
| 36 | assert.throws( |
| 37 | () => parseChecksumManifest("not-a-sha codewhale-linux-x64"), |
| 38 | /Invalid checksum manifest line/, |
| 39 | ); |
| 40 | }); |
| 41 | |
| 42 | test("assertReleaseAssetsFresh rejects missing release assets", () => { |
| 43 | assert.throws( |
| 44 | () => |
| 45 | assertReleaseAssetsFresh( |
| 46 | { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-06-26T00:10:00Z" }] }, |
| 47 | ["codewhale-linux-x64", "codewhale-artifacts-sha256.txt"], |
| 48 | { database_id: 123, created_at: "2026-06-26T00:00:00Z" }, |
| 49 | ), |
| 50 | /missing required release asset/, |
| 51 | ); |
| 52 | }); |
| 53 | |
| 54 | test("assertChecksumManifestIncludes rejects missing bundle manifest and archive rows", () => { |
| 55 | const manifest = parseChecksumManifest( |
| 56 | `${"a".repeat(64)} codewhale-linux-x64.tar.gz`, |
| 57 | ); |
| 58 | |
| 59 | assert.throws( |
| 60 | () => |
| 61 | assertChecksumManifestIncludes( |
| 62 | manifest, |
| 63 | ["codewhale-linux-x64.tar.gz", "codewhale-bundles-sha256.txt"], |
| 64 | "Canonical checksum manifest", |
| 65 | ), |
| 66 | /Canonical checksum manifest is missing codewhale-bundles-sha256\.txt/, |
| 67 | ); |
| 68 | }); |
| 69 | |
| 70 | test("bundle checksum rows use public archive basenames", () => { |
| 71 | const manifest = parseChecksumManifest( |
| 72 | `${"a".repeat(64)} bundles/codewhale-linux-x64.tar.gz`, |
| 73 | ); |
| 74 | |
| 75 | assert.throws( |
| 76 | () => |
| 77 | assertChecksumManifestIncludes( |
| 78 | manifest, |
| 79 | ["codewhale-linux-x64.tar.gz"], |
| 80 | "Bundle checksum manifest", |
| 81 | ), |
| 82 | /Bundle checksum manifest is missing codewhale-linux-x64\.tar\.gz/, |
| 83 | ); |
| 84 | }); |
| 85 | |
| 86 | test("assertReleaseAssetsFresh rejects assets older than the release workflow run", () => { |
| 87 | assert.throws( |
| 88 | () => |
| 89 | assertReleaseAssetsFresh( |
| 90 | { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-06-25T23:59:59Z" }] }, |
| 91 | ["codewhale-linux-x64"], |
| 92 | { database_id: 123, created_at: "2026-06-26T00:00:00Z" }, |
| 93 | ), |
| 94 | /asset set is stale/, |
| 95 | ); |
| 96 | }); |
| 97 | |
| 98 | test("assertReleaseAssetsFresh rejects non-uploaded assets", () => { |
| 99 | assert.throws( |
| 100 | () => |
| 101 | assertReleaseAssetsFresh( |
| 102 | { assets: [{ name: "codewhale-linux-x64", state: "new", updated_at: "2026-06-26T00:10:00Z" }] }, |
| 103 | ["codewhale-linux-x64"], |
| 104 | { database_id: 123, created_at: "2026-06-26T00:00:00Z" }, |
| 105 | ), |
| 106 | /asset set is stale/, |
| 107 | ); |
| 108 | }); |
| 109 | |
| 110 | test("assertReleaseAssetsFresh accepts assets updated by the release workflow run", () => { |
| 111 | assert.doesNotThrow(() => |
| 112 | assertReleaseAssetsFresh( |
| 113 | { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-06-26T00:10:00Z" }] }, |
| 114 | ["codewhale-linux-x64"], |
| 115 | { database_id: 123, created_at: "2026-06-26T00:00:00Z" }, |
| 116 | ), |
| 117 | ); |
| 118 | }); |
| 119 | |
| 120 | test("assertPackageVersionMatchesBinaryVersion allows packaging-only releases only with an explicit override", () => { |
| 121 | assert.doesNotThrow(() => assertPackageVersionMatchesBinaryVersion(pkg.version)); |
| 122 | assert.throws( |
| 123 | () => assertPackageVersionMatchesBinaryVersion("0.0.0-packaging-test"), |
| 124 | /does not match codewhaleBinaryVersion/, |
| 125 | ); |
| 126 | |
| 127 | const previous = process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH; |
| 128 | process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH = "1"; |
| 129 | try { |
| 130 | assert.doesNotThrow(() => assertPackageVersionMatchesBinaryVersion("0.0.0-packaging-test")); |
| 131 | } finally { |
| 132 | if (previous === undefined) { |
| 133 | delete process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH; |
| 134 | } else { |
| 135 | process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH = previous; |
| 136 | } |
| 137 | } |
| 138 | }); |
| 139 | |
| 140 | test("npm publication requires the checkout guard and canonical release-asset gate", () => { |
| 141 | assert.equal( |
| 142 | pkg.scripts.prepublishOnly, |
| 143 | "bash ../../scripts/release/require-release-tag-checkout.sh && " + |
| 144 | "bash ../../scripts/release/verify-release-assets.sh", |
| 145 | ); |
| 146 | }); |
| 147 | |
| 148 | test("full local release fixture satisfies the public asset inventory", () => { |
| 149 | const repoRoot = path.resolve(__dirname, "..", "..", ".."); |
| 150 | const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-assets-")); |
| 151 | const buildDir = path.join(fixtureRoot, "build"); |
| 152 | const outputDir = path.join(fixtureRoot, "assets"); |
| 153 | const executableSuffix = process.platform === "win32" ? ".exe" : ""; |
| 154 | |
| 155 | try { |
| 156 | fs.mkdirSync(buildDir, { recursive: true }); |
| 157 | for (const binary of ["codewhale", "codew", "codewhale-tui"]) { |
| 158 | fs.writeFileSync( |
| 159 | path.join(buildDir, `${binary}${executableSuffix}`), |
| 160 | `fixture:${binary}\n`, |
| 161 | ); |
| 162 | } |
| 163 | |
| 164 | execFileSync( |
| 165 | process.execPath, |
| 166 | [ |
| 167 | path.join(repoRoot, "scripts", "release", "prepare-local-release-assets.js"), |
| 168 | outputDir, |
| 169 | buildDir, |
| 170 | ], |
| 171 | { |
| 172 | env: { ...process.env, DEEPSEEK_TUI_PREPARE_ALL_ASSETS: "1" }, |
| 173 | stdio: "pipe", |
| 174 | }, |
| 175 | ); |
| 176 | |
| 177 | for (const assetName of allReleaseAssetNames()) { |
| 178 | assert.equal( |
| 179 | fs.existsSync(path.join(outputDir, assetName)), |
| 180 | true, |
| 181 | `missing fixture asset ${assetName}`, |
| 182 | ); |
| 183 | } |
| 184 | |
| 185 | const canonicalChecksums = parseChecksumManifest( |
| 186 | fs.readFileSync(path.join(outputDir, CHECKSUM_MANIFEST), "utf8"), |
| 187 | ); |
| 188 | assert.doesNotThrow(() => |
| 189 | assertChecksumManifestIncludes( |
| 190 | canonicalChecksums, |
| 191 | checksummedReleaseAssetNames(), |
| 192 | "Canonical checksum manifest", |
| 193 | ), |
| 194 | ); |
| 195 | |
| 196 | const bundleChecksums = parseChecksumManifest( |
| 197 | fs.readFileSync(path.join(outputDir, BUNDLE_CHECKSUM_MANIFEST), "utf8"), |
| 198 | ); |
| 199 | assert.doesNotThrow(() => |
| 200 | assertChecksumManifestIncludes( |
| 201 | bundleChecksums, |
| 202 | BUNDLE_ASSET_NAMES, |
| 203 | "Bundle checksum manifest", |
| 204 | ), |
| 205 | ); |
| 206 | } finally { |
| 207 | fs.rmSync(fixtureRoot, { recursive: true, force: true }); |
| 208 | } |
| 209 | }); |
| 210 |