返回 DeepSeek-Reasonix
finalize-npm-official-release.test.mjs
根目录 / scripts / finalize-npm-official-release.test.mjs
1 import assert from "node:assert/strict";
2 import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3 import { tmpdir } from "node:os";
4 import { join } from "node:path";
5 import { spawnSync } from "node:child_process";
6 import test from "node:test";
7
8 const candidate = "c46e3af1c2732fe2b3dedb0bd47eb39a629357d2";
9
10 // The fake npm records every dist-tag mutation so a test can assert not just
11 // the outcome but how many registry writes it took to get there — the whole
12 // point of the idempotent path is that a rerun performs none.
13 function run(
14 expectedSha = candidate,
15 publishedSha = candidate,
16 staleReads = 0,
17 { staging = false, forbidRemoval = false } = {},
18 ) {
19 const directory = mkdtempSync(join(tmpdir(), "reasonix-npm-alias-test-"));
20 const npm = join(directory, "npm");
21 const state = join(directory, "state");
22 const writes = join(directory, "writes");
23 writeFileSync(npm, `#!/usr/bin/env node
24 const fs = require("node:fs");
25 const args = process.argv.slice(2);
26 if (args[0] === "view" && args[1].includes("@1.19.2") && args.at(-1) === "--json") {
27 const name = args[1].slice(0, -"@1.19.2".length);
28 console.log(JSON.stringify({ name, version: "1.19.2", gitHead: ${JSON.stringify(publishedSha)}, reasonixCandidateSha: ${JSON.stringify(publishedSha)} }));
29 } else if (args[0] === "view" && args[2] === "dist-tags") {
30 const statePath = process.env.NPM_FAKE_STATE;
31 const reads = fs.existsSync(statePath) ? Number(fs.readFileSync(statePath, "utf8")) : 0;
32 fs.writeFileSync(statePath, String(reads + 1));
33 const stale = reads < Number(process.env.NPM_FAKE_STALE_READS || 0);
34 const tags = stale
35 ? { latest: "1.19.2", canary: "1.19.2-canary.1", next: "1.19.0-rc.3" }
36 : { latest: "1.19.2", canary: "1.19.2", next: "1.19.2" };
37 if (process.env.NPM_FAKE_STAGING === "1") tags["latest-staging"] = "1.19.2";
38 console.log(JSON.stringify(tags));
39 } else if (args[0] === "dist-tag" && (args[1] === "add" || args[1] === "rm")) {
40 fs.appendFileSync(process.env.NPM_FAKE_WRITES, JSON.stringify(args) + "\\n");
41 if (args[1] === "rm" && process.env.NPM_FAKE_FORBID_REMOVAL === "1") {
42 console.error("npm error code E403");
43 console.error("npm error 403 Forbidden");
44 process.exit(1);
45 }
46 process.exit(0);
47 } else {
48 console.error("unexpected npm arguments", JSON.stringify(args));
49 process.exit(2);
50 }
51 `);
52 chmodSync(npm, 0o755);
53 const result = spawnSync(process.execPath, ["scripts/finalize-npm-official-release.mjs", "1.19.2"], {
54 cwd: new URL("..", import.meta.url),
55 encoding: "utf8",
56 env: {
57 ...process.env,
58 EXPECTED_SHA: expectedSha,
59 NPM_FAKE_STATE: state,
60 NPM_FAKE_STALE_READS: String(staleReads),
61 NPM_FAKE_STAGING: staging ? "1" : "0",
62 NPM_FAKE_FORBID_REMOVAL: forbidRemoval ? "1" : "0",
63 NPM_FAKE_WRITES: writes,
64 NPM_TAG_VERIFY_DELAY_MS: "1",
65 PATH: `${directory}:${process.env.PATH}`,
66 },
67 });
68 result.writes = existsSync(writes)
69 ? readFileSync(writes, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
70 : [];
71 return result;
72 }
73
74 test("completes after exact package provenance validation", () => {
75 const result = run();
76 assert.equal(result.status, 0, result.stderr);
77 });
78
79 test("waits for npm alias propagation before continuing", () => {
80 const result = run(candidate, candidate, 2);
81 assert.equal(result.status, 0, result.stderr);
82 });
83
84 test("fails closed before alias mutation when provenance differs", () => {
85 const result = run(candidate, "a".repeat(40));
86 assert.notEqual(result.status, 0);
87 assert.match(result.stderr, /does not match/);
88 assert.deepEqual(result.writes, [], "provenance mismatch must not touch the registry");
89 });
90
91 // Recovery reruns this after the aliases are already correct — including after a
92 // manual realignment, which is exactly what happens when the automation token is
93 // refused. Writing anyway turned a rerun with nothing to do into a 403 (#7342
94 // cluster), so an already-aligned release must perform zero registry writes.
95 test("writes nothing when the official aliases already point at the release", () => {
96 const result = run();
97 assert.equal(result.status, 0, result.stderr);
98 assert.deepEqual(result.writes, [], `unexpected registry writes: ${JSON.stringify(result.writes)}`);
99 });
100
101 // Only the aliases that are actually behind get written.
102 test("writes only the aliases that are missing", () => {
103 const result = run(candidate, candidate, 1);
104 assert.equal(result.status, 0, result.stderr);
105 const added = result.writes.filter((args) => args[1] === "add").map((args) => args.at(-1));
106 assert.ok(added.length > 0, "a stale alias must still be written");
107 assert.ok(!added.includes("latest"), `latest was already current: ${JSON.stringify(result.writes)}`);
108 assert.deepEqual([...new Set(added)].sort(), ["canary", "next"]);
109 });
110
111 // The publisher stages under "<dist-tag>-staging"; cleanup used to name
112 // "official-staging", which nothing creates, so a leftover staging alias
113 // survived on the registry.
114 test("removes the staging alias the publisher actually creates", () => {
115 const result = run(candidate, candidate, 0, { staging: true });
116 assert.equal(result.status, 0, result.stderr);
117 const removed = result.writes.filter((args) => args[1] === "rm");
118 assert.equal(removed.length, 7, `expected one removal per package: ${JSON.stringify(removed)}`);
119 for (const args of removed) {
120 assert.equal(args.at(-1), "latest-staging");
121 }
122 });
123
124 test("completes when npm forbids removal after official aliases converge", () => {
125 const result = run(candidate, candidate, 0, {
126 staging: true,
127 forbidRemoval: true,
128 });
129 assert.equal(result.status, 0, result.stderr);
130 assert.match(result.stderr, /official aliases are already verified/);
131 const removals = result.writes.filter((args) => args[1] === "rm");
132 assert.equal(removals.length, 7);
133 });
134
134 lines Plain Text