返回 DeepSeek-Reasonix
publish.test.mjs
根目录 / npm / publish.test.mjs
1 import assert from "node:assert/strict";
2 import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3 import { tmpdir } from "node:os";
4 import { join } from "node:path";
5 import test from "node:test";
6
7 import {
8 compareDistTagVersions,
9 distTagForVersion,
10 publishPackages,
11 stableDistTagFromEnv,
12 } from "./publish.mjs";
13
14 const candidateSha = "a".repeat(40);
15
16 function splitPackageSpec(spec) {
17 const separator = spec.lastIndexOf("@");
18 return [spec.slice(0, separator), spec.slice(separator + 1)];
19 }
20
21 function fixture(
22 t,
23 version = "1.5.0-canary.42",
24 { forbidCleanup = false, visibilityDelayReads = 0 } = {},
25 ) {
26 const root = mkdtempSync(join(tmpdir(), "reasonix-npm-publish-test-"));
27 t.after(() => rmSync(root, { recursive: true, force: true }));
28
29 const packages = ["@reasonix/cli-linux-x64", "reasonix"].map((name, index) => {
30 const dir = join(root, `package-${index}`);
31 mkdirSync(dir);
32 writeFileSync(
33 join(dir, "package.json"),
34 `${JSON.stringify({ name, version, reasonixCandidateSha: candidateSha })}\n`,
35 );
36 return { name, dir };
37 });
38 const registry = new Map(
39 packages.map(({ name }) => [name, { versions: new Map(), tags: new Map() }]),
40 );
41 const calls = [];
42 const hiddenReads = new Map();
43
44 function packageState(name) {
45 if (!registry.has(name)) {
46 registry.set(name, { versions: new Map(), tags: new Map() });
47 }
48 return registry.get(name);
49 }
50
51 function runner(args, { cwd, missingOk = false } = {}) {
52 calls.push({ args: [...args], cwd });
53 if (args[0] === "view" && args[2]?.startsWith("dist-tags.")) {
54 const state = registry.get(args[1]);
55 if (!state) return missingOk ? null : "";
56 const value = state.tags.get(args[2].slice("dist-tags.".length));
57 return value ? JSON.stringify(value) : "";
58 }
59 if (args[0] === "view") {
60 const [name, requestedVersion] = splitPackageSpec(args[1]);
61 const packageSpec = `${name}@${requestedVersion}`;
62 const remainingHiddenReads = hiddenReads.get(packageSpec) ?? 0;
63 if (remainingHiddenReads > 0) {
64 hiddenReads.set(packageSpec, remainingHiddenReads - 1);
65 return missingOk ? null : "";
66 }
67 const metadata = registry.get(name)?.versions.get(requestedVersion);
68 return metadata ? JSON.stringify(metadata) : missingOk ? null : "";
69 }
70 if (args[0] === "publish") {
71 const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
72 const state = packageState(pkg.name);
73 if (state.versions.has(pkg.version)) {
74 throw new Error(`version already exists: ${pkg.name}@${pkg.version}`);
75 }
76 state.versions.set(pkg.version, {
77 name: pkg.name,
78 version: pkg.version,
79 reasonixCandidateSha: pkg.reasonixCandidateSha,
80 gitHead: pkg.reasonixCandidateSha,
81 });
82 state.tags.set(args[args.indexOf("--tag") + 1], pkg.version);
83 hiddenReads.set(`${pkg.name}@${pkg.version}`, visibilityDelayReads);
84 return "";
85 }
86 if (args[0] === "dist-tag" && args[1] === "add") {
87 const [name, requestedVersion] = splitPackageSpec(args[2]);
88 assert.ok(packageState(name).versions.has(requestedVersion));
89 packageState(name).tags.set(args[3], requestedVersion);
90 return "";
91 }
92 if (args[0] === "dist-tag" && args[1] === "rm") {
93 if (forbidCleanup) {
94 throw new Error("npm dist-tag failed with exit code 1: npm error code E403");
95 }
96 packageState(args[2]).tags.delete(args[3]);
97 return "";
98 }
99 throw new Error(`unsupported fake npm invocation: ${args.join(" ")}`);
100 }
101
102 function publish({ attempts = 2, stableDistTag } = {}) {
103 const options = {
104 packages,
105 version,
106 candidateSha,
107 runner,
108 sleep: (milliseconds) => calls.push({ args: ["sleep", String(milliseconds)] }),
109 log: () => {},
110 };
111 if (attempts !== null) options.attempts = attempts;
112 if (stableDistTag) options.stableDistTag = stableDistTag;
113 return publishPackages(options);
114 }
115
116 function addVersion(name, publishedVersion = version, sha = candidateSha) {
117 packageState(name).versions.set(publishedVersion, {
118 name,
119 version: publishedVersion,
120 reasonixCandidateSha: sha,
121 gitHead: sha,
122 });
123 }
124
125 return { packages, registry, calls, publish, addVersion, revealPackages: () => hiddenReads.clear() };
126 }
127
128 test("reuses a fully published npm candidate without republishing", (t) => {
129 const fx = fixture(t);
130 for (const { name } of fx.packages) {
131 fx.addVersion(name);
132 fx.registry.get(name).tags.set("canary", "1.5.0-canary.42");
133 }
134
135 assert.deepEqual(fx.publish(), {
136 distTag: "canary",
137 version: "1.5.0-canary.42",
138 });
139 assert.equal(fx.calls.filter(({ args }) => args[0] === "publish").length, 0);
140 });
141
142 test("fills a partially published package set before advancing canary", (t) => {
143 const fx = fixture(t);
144 fx.addVersion(fx.packages[0].name);
145 for (const { name } of fx.packages) {
146 fx.registry.get(name).tags.set("canary", "1.5.0-canary.41");
147 }
148
149 fx.publish();
150
151 const publishes = fx.calls.filter(({ args }) => args[0] === "publish");
152 assert.equal(publishes.length, 1);
153 assert.equal(publishes[0].cwd, fx.packages[1].dir);
154 assert.ok(publishes[0].args.includes("--provenance"), "every publish attaches a provenance attestation");
155 for (const { name } of fx.packages) {
156 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
157 assert.equal(fx.registry.get(name).tags.has("canary-staging"), false);
158 }
159 });
160
161 test("publishes the exact prepared tarball instead of rebuilding a package directory", (t) => {
162 const fx = fixture(t);
163 const tarball = join(fx.packages[0].dir, "reasonix-cli-linux-x64-1.5.0-canary.42.tgz");
164 writeFileSync(tarball, "sealed tarball bytes");
165 fx.packages[0].tarball = tarball;
166
167 fx.publish();
168
169 const call = fx.calls.find(({ args }) => args[0] === "publish" && args[1] === tarball);
170 assert.ok(call, "npm publish must receive the sealed tarball path");
171 assert.equal(call.args.filter(argument => argument === tarball).length, 1);
172 });
173
174 test("waits through multi-minute npm registry visibility lag", (t) => {
175 const fx = fixture(t, "1.5.0-canary.42", { visibilityDelayReads: 45 });
176
177 assert.doesNotThrow(() => fx.publish({ attempts: null }));
178 const firstSleep = fx.calls.findIndex(({ args }) => args[0] === "sleep");
179 assert.equal(fx.calls.slice(0, firstSleep).filter(({ args }) => args[0] === "publish").length, fx.packages.length);
180 // One shared visibility window, not one sequential wait per package.
181 assert.equal(fx.calls.filter(({ args }) => args[0] === "sleep").length, 45);
182 for (const { name } of fx.packages) {
183 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
184 }
185 });
186
187 test("a visibility timeout uploads the full set but preserves aliases and staging for recovery", (t) => {
188 const fx = fixture(t, "1.5.0-canary.42", { visibilityDelayReads: 5 });
189 for (const { name } of fx.packages) fx.registry.get(name).tags.set("canary", "1.5.0-canary.41");
190
191 assert.throws(() => fx.publish(), /did not become visible/);
192 assert.equal(fx.calls.filter(({ args }) => args[0] === "publish").length, fx.packages.length);
193 assert.equal(fx.calls.some(({ args }) => args[0] === "dist-tag"), false);
194 for (const { name } of fx.packages) {
195 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.41");
196 assert.equal(fx.registry.get(name).tags.get("canary-staging"), "1.5.0-canary.42");
197 }
198 const uploaded = fx.calls.filter(({ args }) => args[0] === "publish").length;
199 // Once npm exposes the uploaded versions, recovery reuses them.
200 fx.revealPackages();
201 assert.doesNotThrow(() => fx.publish());
202 assert.equal(fx.calls.filter(({ args }) => args[0] === "publish").length, uploaded);
203 for (const { name } of fx.packages) {
204 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
205 assert.equal(fx.registry.get(name).tags.has("canary-staging"), false);
206 }
207 });
208
209 test("rejects an immutable package owned by another candidate", (t) => {
210 const fx = fixture(t);
211 fx.addVersion(fx.packages[0].name, "1.5.0-canary.42", "b".repeat(40));
212
213 assert.throws(
214 () => fx.publish(),
215 /belongs to candidate b{40}, expected a{40}/,
216 );
217 assert.equal(fx.calls.filter(({ args }) => args[0] === "publish").length, 0);
218 });
219
220 test("stale recovery publishes missing packages without rolling canary back", (t) => {
221 const fx = fixture(t);
222 fx.addVersion(fx.packages[0].name);
223 for (const { name } of fx.packages) {
224 fx.addVersion(name, "1.5.0-canary.43");
225 fx.registry.get(name).tags.set("canary", "1.5.0-canary.43");
226 }
227
228 fx.publish();
229
230 assert.ok(
231 fx.registry
232 .get(fx.packages[1].name)
233 .versions.has("1.5.0-canary.42"),
234 );
235 for (const { name } of fx.packages) {
236 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.43");
237 }
238 assert.equal(
239 fx.calls.some(
240 ({ args }) =>
241 args[0] === "dist-tag" &&
242 args[1] === "add" &&
243 args[3] === "canary",
244 ),
245 false,
246 );
247 });
248
249 test("accepts legacy packages whose gitHead proves the candidate", (t) => {
250 const fx = fixture(t);
251 for (const { name } of fx.packages) {
252 fx.registry.get(name).versions.set("1.5.0-canary.42", {
253 name,
254 version: "1.5.0-canary.42",
255 gitHead: candidateSha,
256 });
257 fx.registry.get(name).tags.set("canary", "1.5.0-canary.42");
258 }
259
260 assert.doesNotThrow(() => fx.publish());
261 });
262
263 test("does not fail a completed publish when npm forbids staging cleanup", (t) => {
264 const fx = fixture(t, "1.5.0-canary.42", { forbidCleanup: true });
265
266 assert.doesNotThrow(() => fx.publish());
267 for (const { name } of fx.packages) {
268 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
269 assert.equal(
270 fx.registry.get(name).tags.get("canary-staging"),
271 "1.5.0-canary.42",
272 );
273 }
274 });
275
276 test("compares channel versions without integer truncation", () => {
277 assert.equal(
278 compareDistTagVersions(
279 "canary",
280 "1.5.0-canary.100000000000000000000",
281 "1.5.0-canary.99999999999999999999",
282 ),
283 1,
284 );
285 assert.equal(
286 compareDistTagVersions("latest", "2.0.0", "10.0.0"),
287 -1,
288 );
289 assert.equal(
290 compareDistTagVersions("next", "1.5.0-rc.10", "1.5.0-rc.2"),
291 1,
292 );
293 });
294
295 test("a stable publish moves latest unless the frozen tag is selected", (t) => {
296 const normal = fixture(t, "1.6.0");
297 assert.deepEqual(normal.publish(), { distTag: "latest", version: "1.6.0" });
298 for (const { name } of normal.packages) {
299 assert.equal(normal.registry.get(name).tags.get("latest"), "1.6.0");
300 }
301
302 const frozen = fixture(t, "1.6.0");
303 for (const { name } of frozen.packages) frozen.registry.get(name).tags.set("latest", "2.0.0");
304 assert.deepEqual(frozen.publish({ stableDistTag: "legacy-v1" }), {
305 distTag: "legacy-v1",
306 version: "1.6.0",
307 });
308 for (const { name } of frozen.packages) {
309 const tags = frozen.registry.get(name).tags;
310 assert.equal(tags.get("latest"), "2.0.0");
311 assert.equal(tags.get("legacy-v1"), "1.6.0");
312 assert.equal(tags.has("legacy-v1-staging"), false);
313 }
314 const written = frozen.calls
315 .filter(({ args }) => args[0] === "publish" || (args[0] === "dist-tag" && args[1] === "add"))
316 .flatMap(({ args }) => args);
317 assert.equal(written.includes("latest"), false);
318 });
319
320 test("the frozen tag never leaves the stable version space", () => {
321 assert.equal(distTagForVersion("1.6.0", "legacy-v1"), "legacy-v1");
322 assert.equal(distTagForVersion("1.6.0-rc.1", "legacy-v1"), "next");
323 assert.throws(() => distTagForVersion("1.6.0", "v1"), /invalid stable npm dist-tag/);
324 assert.throws(() => compareDistTagVersions("legacy-v1", "1.6.0-rc.1", "1.5.0"), /does not belong/);
325 });
326
327 test("the stable dist-tag comes from the environment and defaults to latest", () => {
328 assert.equal(stableDistTagFromEnv({}), "latest");
329 assert.equal(stableDistTagFromEnv({ NPM_STABLE_DIST_TAG: "" }), "latest");
330 assert.equal(stableDistTagFromEnv({ NPM_STABLE_DIST_TAG: "legacy-v1" }), "legacy-v1");
331 assert.throws(() => stableDistTagFromEnv({ NPM_STABLE_DIST_TAG: "v1" }), /must be latest or legacy-v1/);
332 });
333
333 lines Plain Text