import assert from "node:assert/strict"; import { test } from "node:test"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { buildAgentNode } from "./build-agent-node.mjs"; test("the release builder injects the exact version into an executable binary", { timeout: 360_000 }, async () => { const root = await mkdtemp(join(tmpdir(), "agent-node-version-")); try { for (const version of ["dev", "v1.2.3-rc.4", "sha-0123456789ab"]) { const binary = join(root, process.platform === "win32" ? `${version}.exe` : version); // Run the same builder as the release workflow, using this test host's // architecture; cross-platform archive builds are a separate gate. const env = { ...process.env }; delete env.GOOS; delete env.GOARCH; buildAgentNode(version, binary, { env }); const result = spawnSync(binary, ["--version"], { cwd: root, env: process.platform === "win32" ? { SystemRoot: process.env.SystemRoot } : {}, encoding: "utf8", timeout: 5000, }); assert.equal(result.error, undefined); assert.equal(result.status, 0); assert.equal(result.stderr, ""); assert.equal(result.stdout, `openlinker-agent-node/${version}\n`); } } finally { await rm(root, { recursive: true, force: true }); } }); test("invalid versions fail before any build can run", () => { for (const version of [undefined, "", "latest", "v1.2.3 extra", "openlinker-cli/v1.2.3", "sha-NOTHEX", "v1.2.3\n"]) { assert.throws(() => buildAgentNode(version, "unused", { env: { PATH: "" } }), /version must/); } assert.throws(() => buildAgentNode("dev", ""), /output path is required/); }); const releaseGate = fileURLToPath(new URL("./check-release-upgrade-readiness.mjs", import.meta.url)); function checkRelease(args, env = process.env) { const result = spawnSync(process.execPath, [releaseGate, ...args], { env, encoding: "utf8", timeout: 5000, }); assert.equal(result.error, undefined); return result; } test("the executable release gate retains canonical pre-1.0 test prereleases", () => { for (const tag of ["v0.0.0-alpha.0", "v0.1.59-alpha.1", "v0.1.59-beta.2", "v0.1.59-rc.1", "v0.20.100-rc.10"]) { const result = checkRelease([tag]); assert.equal(result.status, 0, tag); assert.equal(result.stderr, "", tag); assert.match(result.stdout, /^NODE_TEST_PRERELEASE_ALLOWED:/, tag); } }); test("the executable release gate rejects missing, untested stable, production and malformed versions", () => { for (const args of [ [], [""], ["v0.1.59"], ["v0.2.1"], ["v0.3.0"], ["v0.2.0\n"], ["v0.2.0", "extra"], ["v1.0.0"], ["v1.0.0-rc.1"], ["v2.0.0-alpha.1"], ["dev"], ["latest"], ["sha-0123456789ab"], ["0.1.59-rc.1"], ["refs/tags/v0.1.59-rc.1"], ["v0.01.59-rc.1"], ["v0.1.059-rc.1"], ["v0.1.59-rc.01"], ["v00.1.59-rc.1"], ["v0.1.59-preview.1"], ["v0.1.59-RC.1"], ["v0.1.59-rc"], ["v0.1.59-rc.-1"], ["v0.1.59-rc.1.2"], ["v0.1.59-rc.1+build.1"], ["v0.1.59-rc.1\n"], [" v0.1.59-rc.1"], ["v0.1.59-rc.1", "extra"], ]) { const result = checkRelease(args); assert.equal(result.status, 1, JSON.stringify(args)); assert.equal(result.stdout, "", JSON.stringify(args)); assert.match(result.stderr, /^NODE_SUPPORTED_RELEASE_REQUIRED:/, JSON.stringify(args)); } }); test("the exact supported stable version remains a candidate pending artifact acceptance", () => { const result = checkRelease(["v0.2.0"]); assert.equal(result.status, 0); assert.match(result.stdout, /^NODE_CONTROLLED_RELEASE_CANDIDATE:/); }); test("environment values cannot replace or bypass the explicit release tag", () => { const env = { ...process.env, GITHUB_REF_NAME: "v0.1.59-rc.1", NODE_UPGRADE_COMPATIBILITY_READY: "true", ALLOW_NODE_UPGRADE_WITHOUT_CORE: "1", }; for (const args of [[], ["v0.1.59"], ["v1.0.0-rc.1"]]) { const result = checkRelease(args, env); assert.equal(result.status, 1); assert.equal(result.stdout, ""); } }); test("release workflow gates the actual tag and keeps tested six-platform packaging behind artifact acceptance", async () => { // Wiring coverage, distinct from the real gate and binary executions above. const workflow = await readFile(new URL("../.github/workflows/release.yml", import.meta.url), "utf8"); const packaging = workflow.slice(workflow.indexOf(" package-binaries:"), workflow.indexOf(" publish-release:")); assert.match(packaging, /if: github\.ref_type == 'tag'\n\s+run: node scripts\/check-release-upgrade-readiness\.mjs "\$GITHUB_REF_NAME"/); assert.ok(packaging.indexOf("check-release-upgrade-readiness.mjs") < packaging.indexOf("go mod download")); assert.ok(packaging.includes("go test ./...")); assert.ok(packaging.indexOf("go test ./...") < packaging.indexOf("- name: Package binary")); assert.ok(packaging.includes("node --test scripts/build-agent-node.test.mjs")); assert.deepEqual([...packaging.matchAll(/- goos: (\w+)\n\s+goarch: (\w+)/g)].map((match) => `${match[1]}/${match[2]}`), [ "linux/amd64", "linux/arm64", "darwin/amd64", "darwin/arm64", "windows/amd64", "windows/arm64", ]); assert.ok(packaging.includes('version="sha-${GITHUB_SHA::12}"')); assert.ok(packaging.includes("sha256sum")); assert.ok(packaging.includes(".sha256")); assert.match(workflow, /publish-release:[\s\S]*?always\(\) && github\.ref_type == 'tag'/); assert.match(workflow, /needs: \[package-binaries, controlled-upgrade\]/); assert.match(workflow, /controlled-upgrade:[\s\S]*?needs: package-binaries[\s\S]*?uses: \.\/\.github\/workflows\/controlled-upgrade\.yml/); assert.match(workflow, /artifact_name: openlinker-agent-node-v0\.2\.0-linux-amd64/); assert.match(workflow, /prerelease="false"/); assert.match(workflow, /\[\[ "\$\{tag\}" == \*-\* \]\] && prerelease="true"/); assert.match(workflow, /gh release create "\$\{tag\}"[^\n]*--prerelease="\$prerelease"/); assert.match(workflow, /gh release edit "\$\{tag\}" --prerelease="\$prerelease"/); });