返回 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 publishPackages,
10 } from "./publish.mjs";
11
12 const candidateSha = "a".repeat(40);
13
14 function splitPackageSpec(spec) {
15 const separator = spec.lastIndexOf("@");
16 return [spec.slice(0, separator), spec.slice(separator + 1)];
17 }
18
19 function fixture(
20 t,
21 version = "1.5.0-canary.42",
22 { forbidCleanup = false, visibilityDelayReads = 0 } = {},
23 ) {
24 const root = mkdtempSync(join(tmpdir(), "reasonix-npm-publish-test-"));
25 t.after(() => rmSync(root, { recursive: true, force: true }));
26
27 const packages = ["@reasonix/cli-linux-x64", "reasonix"].map((name, index) => {
28 const dir = join(root, `package-${index}`);
29 mkdirSync(dir);
30 writeFileSync(
31 join(dir, "package.json"),
32 `${JSON.stringify({ name, version, reasonixCandidateSha: candidateSha })}\n`,
33 );
34 return { name, dir };
35 });
36 const registry = new Map(
37 packages.map(({ name }) => [name, { versions: new Map(), tags: new Map() }]),
38 );
39 const calls = [];
40 const hiddenReads = new Map();
41
42 function packageState(name) {
43 if (!registry.has(name)) {
44 registry.set(name, { versions: new Map(), tags: new Map() });
45 }
46 return registry.get(name);
47 }
48
49 function runner(args, { cwd, missingOk = false } = {}) {
50 calls.push({ args: [...args], cwd });
51 if (args[0] === "view" && args[2]?.startsWith("dist-tags.")) {
52 const state = registry.get(args[1]);
53 if (!state) return missingOk ? null : "";
54 const value = state.tags.get(args[2].slice("dist-tags.".length));
55 return value ? JSON.stringify(value) : "";
56 }
57 if (args[0] === "view") {
58 const [name, requestedVersion] = splitPackageSpec(args[1]);
59 const packageSpec = `${name}@${requestedVersion}`;
60 const remainingHiddenReads = hiddenReads.get(packageSpec) ?? 0;
61 if (remainingHiddenReads > 0) {
62 hiddenReads.set(packageSpec, remainingHiddenReads - 1);
63 return missingOk ? null : "";
64 }
65 const metadata = registry.get(name)?.versions.get(requestedVersion);
66 return metadata ? JSON.stringify(metadata) : missingOk ? null : "";
67 }
68 if (args[0] === "publish") {
69 const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
70 const state = packageState(pkg.name);
71 if (state.versions.has(pkg.version)) {
72 throw new Error(`version already exists: ${pkg.name}@${pkg.version}`);
73 }
74 state.versions.set(pkg.version, {
75 name: pkg.name,
76 version: pkg.version,
77 reasonixCandidateSha: pkg.reasonixCandidateSha,
78 gitHead: pkg.reasonixCandidateSha,
79 });
80 state.tags.set(args[args.indexOf("--tag") + 1], pkg.version);
81 hiddenReads.set(`${pkg.name}@${pkg.version}`, visibilityDelayReads);
82 return "";
83 }
84 if (args[0] === "dist-tag" && args[1] === "add") {
85 const [name, requestedVersion] = splitPackageSpec(args[2]);
86 assert.ok(packageState(name).versions.has(requestedVersion));
87 packageState(name).tags.set(args[3], requestedVersion);
88 return "";
89 }
90 if (args[0] === "dist-tag" && args[1] === "rm") {
91 if (forbidCleanup) {
92 throw new Error("npm dist-tag failed with exit code 1: npm error code E403");
93 }
94 packageState(args[2]).tags.delete(args[3]);
95 return "";
96 }
97 throw new Error(`unsupported fake npm invocation: ${args.join(" ")}`);
98 }
99
100 function publish({ attempts = 2 } = {}) {
101 const options = {
102 packages,
103 version,
104 candidateSha,
105 runner,
106 sleep: () => {},
107 log: () => {},
108 };
109 if (attempts !== null) options.attempts = attempts;
110 return publishPackages(options);
111 }
112
113 function addVersion(name, publishedVersion = version, sha = candidateSha) {
114 packageState(name).versions.set(publishedVersion, {
115 name,
116 version: publishedVersion,
117 reasonixCandidateSha: sha,
118 gitHead: sha,
119 });
120 }
121
122 return { packages, registry, calls, publish, addVersion };
123 }
124
125 test("reuses a fully published npm candidate without republishing", (t) => {
126 const fx = fixture(t);
127 for (const { name } of fx.packages) {
128 fx.addVersion(name);
129 fx.registry.get(name).tags.set("canary", "1.5.0-canary.42");
130 }
131
132 assert.deepEqual(fx.publish(), {
133 distTag: "canary",
134 version: "1.5.0-canary.42",
135 });
136 assert.equal(fx.calls.filter(({ args }) => args[0] === "publish").length, 0);
137 });
138
139 test("fills a partially published package set before advancing canary", (t) => {
140 const fx = fixture(t);
141 fx.addVersion(fx.packages[0].name);
142 for (const { name } of fx.packages) {
143 fx.registry.get(name).tags.set("canary", "1.5.0-canary.41");
144 }
145
146 fx.publish();
147
148 const publishes = fx.calls.filter(({ args }) => args[0] === "publish");
149 assert.equal(publishes.length, 1);
150 assert.equal(publishes[0].cwd, fx.packages[1].dir);
151 for (const { name } of fx.packages) {
152 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
153 assert.equal(fx.registry.get(name).tags.has("canary-staging"), false);
154 }
155 });
156
157 test("waits through multi-minute npm registry visibility lag", (t) => {
158 const fx = fixture(t, "1.5.0-canary.42", { visibilityDelayReads: 18 });
159
160 assert.doesNotThrow(() => fx.publish({ attempts: null }));
161 for (const { name } of fx.packages) {
162 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
163 }
164 });
165
166 test("rejects an immutable package owned by another candidate", (t) => {
167 const fx = fixture(t);
168 fx.addVersion(fx.packages[0].name, "1.5.0-canary.42", "b".repeat(40));
169
170 assert.throws(
171 () => fx.publish(),
172 /belongs to candidate b{40}, expected a{40}/,
173 );
174 assert.equal(fx.calls.filter(({ args }) => args[0] === "publish").length, 0);
175 });
176
177 test("stale recovery publishes missing packages without rolling canary back", (t) => {
178 const fx = fixture(t);
179 fx.addVersion(fx.packages[0].name);
180 for (const { name } of fx.packages) {
181 fx.addVersion(name, "1.5.0-canary.43");
182 fx.registry.get(name).tags.set("canary", "1.5.0-canary.43");
183 }
184
185 fx.publish();
186
187 assert.ok(
188 fx.registry
189 .get(fx.packages[1].name)
190 .versions.has("1.5.0-canary.42"),
191 );
192 for (const { name } of fx.packages) {
193 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.43");
194 }
195 assert.equal(
196 fx.calls.some(
197 ({ args }) =>
198 args[0] === "dist-tag" &&
199 args[1] === "add" &&
200 args[3] === "canary",
201 ),
202 false,
203 );
204 });
205
206 test("accepts legacy packages whose gitHead proves the candidate", (t) => {
207 const fx = fixture(t);
208 for (const { name } of fx.packages) {
209 fx.registry.get(name).versions.set("1.5.0-canary.42", {
210 name,
211 version: "1.5.0-canary.42",
212 gitHead: candidateSha,
213 });
214 fx.registry.get(name).tags.set("canary", "1.5.0-canary.42");
215 }
216
217 assert.doesNotThrow(() => fx.publish());
218 });
219
220 test("does not fail a completed publish when npm forbids staging cleanup", (t) => {
221 const fx = fixture(t, "1.5.0-canary.42", { forbidCleanup: true });
222
223 assert.doesNotThrow(() => fx.publish());
224 for (const { name } of fx.packages) {
225 assert.equal(fx.registry.get(name).tags.get("canary"), "1.5.0-canary.42");
226 assert.equal(
227 fx.registry.get(name).tags.get("canary-staging"),
228 "1.5.0-canary.42",
229 );
230 }
231 });
232
233 test("compares channel versions without integer truncation", () => {
234 assert.equal(
235 compareDistTagVersions(
236 "canary",
237 "1.5.0-canary.100000000000000000000",
238 "1.5.0-canary.99999999999999999999",
239 ),
240 1,
241 );
242 assert.equal(
243 compareDistTagVersions("latest", "2.0.0", "10.0.0"),
244 -1,
245 );
246 assert.equal(
247 compareDistTagVersions("next", "1.5.0-rc.10", "1.5.0-rc.2"),
248 1,
249 );
250 });
251
251 lines Plain Text