返回 CodeWhale
install.test.js
根目录 / npm / codewhale / test / install.test.js
1 const assert = require("node:assert/strict");
2 const crypto = require("node:crypto");
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 installScript = fs.readFileSync(
9 path.join(__dirname, "..", "scripts", "install.js"),
10 "utf8",
11 );
12 const { installFailureHint, _internal } = require("../scripts/install");
13 const { _internal: glibcInternal } = require("../scripts/preflight-glibc");
14
15 function sha256(content) {
16 return crypto.createHash("sha256").update(content).digest("hex");
17 }
18
19 async function makeTempDir(t) {
20 const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "codewhale-install-test-"));
21 t.after(() => fs.promises.rm(dir, { force: true, recursive: true }));
22 return dir;
23 }
24
25 async function exists(file) {
26 return fs.promises.access(file).then(
27 () => true,
28 () => false,
29 );
30 }
31
32 async function withoutForcedDownload(callback) {
33 const previousCodewhale = process.env.CODEWHALE_FORCE_DOWNLOAD;
34 const previousTui = process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
35 const previousLegacy = process.env.DEEPSEEK_FORCE_DOWNLOAD;
36 delete process.env.CODEWHALE_FORCE_DOWNLOAD;
37 delete process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
38 delete process.env.DEEPSEEK_FORCE_DOWNLOAD;
39 try {
40 return await callback();
41 } finally {
42 if (previousCodewhale === undefined) {
43 delete process.env.CODEWHALE_FORCE_DOWNLOAD;
44 } else {
45 process.env.CODEWHALE_FORCE_DOWNLOAD = previousCodewhale;
46 }
47 if (previousTui === undefined) {
48 delete process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
49 } else {
50 process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD = previousTui;
51 }
52 if (previousLegacy === undefined) {
53 delete process.env.DEEPSEEK_FORCE_DOWNLOAD;
54 } else {
55 process.env.DEEPSEEK_FORCE_DOWNLOAD = previousLegacy;
56 }
57 }
58 }
59
60 test("install script checks Node support before loading helpers", () => {
61 const guardIndex = installScript.indexOf("assertSupportedNode();");
62 const firstRequireIndex = installScript.indexOf("require(");
63
64 assert.notEqual(guardIndex, -1);
65 assert.notEqual(firstRequireIndex, -1);
66 assert.ok(guardIndex < firstRequireIndex);
67 });
68
69 test("install script remains parseable before the Node support guard runs", () => {
70 assert.equal(installScript.includes("??"), false);
71 assert.equal(installScript.includes("?."), false);
72 });
73
74 test("native shortcut has its own platform asset and installed path", () => {
75 const paths = _internal.binaryPaths();
76 assert.match(paths.codew.asset, /^codew-/);
77 assert.equal(path.basename(paths.codew.target), process.platform === "win32" ? "codew.exe" : "codew");
78 assert.notEqual(paths.codew.target, paths.codewhale.target);
79 });
80
81 test("install failure hint explains release base override for blocked GitHub downloads", () => {
82 const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
83 delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
84 try {
85 const error = Object.assign(
86 new Error(
87 "fetch https://github.com/Hmbown/CodeWhale/releases/download/v0.8.19/codewhale-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
88 ),
89 { code: "ENOTFOUND" },
90 );
91
92 const hint = installFailureHint(error);
93
94 assert.match(hint, /CODEWHALE_RELEASE_BASE_URL/);
95 assert.match(hint, /codewhale-artifacts-sha256\.txt/);
96 assert.match(hint, /platform binaries/);
97 assert.match(hint, /#npm-binary-download-times-out/);
98 } finally {
99 if (previous === undefined) {
100 delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
101 } else {
102 process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
103 }
104 }
105 });
106
107 test("install failure hint checks configured release base when override is already set", () => {
108 const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
109 process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = "https://mirror.example/deepseek/";
110 try {
111 const error = Object.assign(new Error("download stalled"), {
112 code: "EDOWNLOADTIMEOUT",
113 });
114
115 const hint = installFailureHint(error);
116
117 assert.match(hint, /resolves to https:\/\/mirror\.example\/deepseek\//);
118 assert.match(hint, /codewhale-artifacts-sha256\.txt/);
119 assert.doesNotMatch(hint, /If GitHub is unavailable/);
120 } finally {
121 if (previous === undefined) {
122 delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
123 } else {
124 process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
125 }
126 }
127 });
128
129 test("glibc preflight message is Codewhale-branded and actionable", () => {
130 const message = glibcInternal.glibcCompatibilityMessage([2, 39, 0], [2, 35, 0]);
131
132 assert.match(message, /Prebuilt Codewhale Linux binaries require GLIBC_2\.39/);
133 assert.match(message, /this system has glibc 2\.35/);
134 assert.match(message, /cargo install codewhale-cli --locked/);
135 assert.match(message, /ln -sf .*codewhale.*codew/);
136 assert.doesNotMatch(message, /cargo install codewhale-tui/);
137 assert.match(message, /Linux x64 release asset is a static \(musl\) build/);
138 assert.match(message, /Linux arm64 asset is a GNU libc build/);
139 assert.match(message, /CODEWHALE_SKIP_GLIBC_CHECK=1/);
140 });
141
142 test("glibc preflight accepts canonical and legacy skip env vars", () => {
143 const previousCodewhale = process.env.CODEWHALE_SKIP_GLIBC_CHECK;
144 const previousTui = process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK;
145 const previousLegacy = process.env.DEEPSEEK_SKIP_GLIBC_CHECK;
146 delete process.env.CODEWHALE_SKIP_GLIBC_CHECK;
147 delete process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK;
148 delete process.env.DEEPSEEK_SKIP_GLIBC_CHECK;
149 try {
150 assert.equal(glibcInternal.skipGlibcCheck(), false);
151 process.env.CODEWHALE_SKIP_GLIBC_CHECK = "1";
152 assert.equal(glibcInternal.skipGlibcCheck(), true);
153 delete process.env.CODEWHALE_SKIP_GLIBC_CHECK;
154 process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK = "1";
155 assert.equal(glibcInternal.skipGlibcCheck(), true);
156 } finally {
157 if (previousCodewhale === undefined) {
158 delete process.env.CODEWHALE_SKIP_GLIBC_CHECK;
159 } else {
160 process.env.CODEWHALE_SKIP_GLIBC_CHECK = previousCodewhale;
161 }
162 if (previousTui === undefined) {
163 delete process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK;
164 } else {
165 process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK = previousTui;
166 }
167 if (previousLegacy === undefined) {
168 delete process.env.DEEPSEEK_SKIP_GLIBC_CHECK;
169 } else {
170 process.env.DEEPSEEK_SKIP_GLIBC_CHECK = previousLegacy;
171 }
172 }
173 });
174
175 test("ensureBinary adopts a manually placed target binary after checksum validation", async (t) => {
176 const dir = await makeTempDir(t);
177 const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
178 const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
179 const version = "0.8.25";
180 const content = Buffer.from("manual codewhale binary");
181 let checksumLoads = 0;
182
183 await fs.promises.writeFile(target, content, { mode: 0o600 });
184 await fs.promises.writeFile(`${target}.version`, "0.8.24", "utf8");
185
186 const result = await withoutForcedDownload(() =>
187 _internal.ensureBinary(target, assetName, version, "Hmbown/CodeWhale", async () => {
188 checksumLoads += 1;
189 return new Map([[assetName, sha256(content)]]);
190 }),
191 );
192
193 assert.equal(result, target);
194 assert.equal(checksumLoads, 1);
195 assert.equal(await fs.promises.readFile(`${target}.version`, "utf8"), version);
196 if (process.platform !== "win32") {
197 assert.notEqual((await fs.promises.stat(target)).mode & 0o111, 0);
198 }
199 });
200
201 test("ensureBinary adopts an official release-named binary placed in downloads", async (t) => {
202 const dir = await makeTempDir(t);
203 const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
204 const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
205 const assetPath = path.join(dir, assetName);
206 const version = "0.8.25";
207 const content = Buffer.from("official release binary");
208
209 await fs.promises.writeFile(assetPath, content);
210
211 const result = await withoutForcedDownload(() =>
212 _internal.ensureBinary(target, assetName, version, "Hmbown/CodeWhale", async () =>
213 new Map([[assetName, sha256(content)]]),
214 ),
215 );
216
217 assert.equal(result, target);
218 assert.equal(await exists(target), true);
219 assert.equal(await exists(assetPath), false);
220 assert.equal(await fs.promises.readFile(`${target}.version`, "utf8"), version);
221 });
222
223 test("manual binaries with mismatched checksums are not adopted", async (t) => {
224 const dir = await makeTempDir(t);
225 const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
226 const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
227 const content = Buffer.from("wrong binary bytes");
228
229 await fs.promises.writeFile(target, content);
230
231 const adopted = await _internal.adoptExistingBinaryIfValid(
232 target,
233 assetName,
234 "0.8.25",
235 async () => new Map([[assetName, sha256(Buffer.from("different bytes"))]]),
236 `${target}.version`,
237 );
238
239 assert.equal(adopted, false);
240 assert.equal(await exists(`${target}.version`), false);
241 });
242
243 test("resolvePackageVersion honors codewhaleBinaryVersion precedence (#3769)", () => {
244 const { resolvePackageVersion } = _internal;
245
246 // codewhaleBinaryVersion wins over deepseekBinaryVersion and pkg.version.
247 assert.equal(
248 resolvePackageVersion(
249 {
250 codewhaleBinaryVersion: "1.2.3",
251 deepseekBinaryVersion: "0.0.1",
252 version: "9.9.9",
253 },
254 {},
255 ),
256 "1.2.3",
257 );
258
259 // Falls back to deepseekBinaryVersion, then pkg.version.
260 assert.equal(
261 resolvePackageVersion({ deepseekBinaryVersion: "0.0.1", version: "9.9.9" }, {}),
262 "0.0.1",
263 );
264 assert.equal(resolvePackageVersion({ version: "9.9.9" }, {}), "9.9.9");
265
266 assert.equal(
267 resolvePackageVersion(
268 { codewhaleBinaryVersion: "1.2.3" },
269 {
270 CODEWHALE_VERSION: "6.6.6",
271 DEEPSEEK_TUI_VERSION: "7.7.7",
272 DEEPSEEK_VERSION: "8.8.8",
273 },
274 ),
275 "6.6.6",
276 );
277
278 // Legacy env vars still take precedence over package fields, unchanged.
279 assert.equal(
280 resolvePackageVersion(
281 { codewhaleBinaryVersion: "1.2.3", version: "9.9.9" },
282 { DEEPSEEK_TUI_VERSION: "7.7.7" },
283 ),
284 "7.7.7",
285 );
286 assert.equal(
287 resolvePackageVersion(
288 { codewhaleBinaryVersion: "1.2.3" },
289 { DEEPSEEK_VERSION: "8.8.8" },
290 ),
291 "8.8.8",
292 );
293 });
294
295 test("canonical Codewhale installer variables outrank legacy aliases", () => {
296 assert.equal(
297 _internal.resolveRepo({
298 CODEWHALE_GITHUB_REPO: "example/codewhale",
299 DEEPSEEK_TUI_GITHUB_REPO: "legacy/tui",
300 DEEPSEEK_GITHUB_REPO: "legacy/root",
301 }),
302 "example/codewhale",
303 );
304 assert.equal(
305 _internal.isOptionalInstall([], {
306 CODEWHALE_OPTIONAL_INSTALL: "1",
307 }),
308 true,
309 );
310 assert.equal(
311 _internal.isQuietInstall({ CODEWHALE_QUIET_INSTALL: "1" }),
312 true,
313 );
314 assert.equal(
315 _internal.shouldForceDownload({ CODEWHALE_FORCE_DOWNLOAD: "1" }),
316 true,
317 );
318 assert.equal(
319 _internal.shouldDisableInstall({ CODEWHALE_DISABLE_INSTALL: "1" }),
320 true,
321 );
322 assert.equal(
323 _internal.downloadTimeoutMs("runtime", {
324 CODEWHALE_DOWNLOAD_TIMEOUT_MS: "1234",
325 DEEPSEEK_TUI_DOWNLOAD_TIMEOUT_MS: "9999",
326 }),
327 1234,
328 );
329 assert.equal(
330 _internal.downloadStallMs("runtime", {
331 CODEWHALE_DOWNLOAD_STALL_MS: "5678",
332 DEEPSEEK_TUI_DOWNLOAD_STALL_MS: "9999",
333 }),
334 5678,
335 );
336 });
337
338 test("httpRequest handles invalid URL parsing errors", async () => {
339 const { httpRequest } = _internal;
340 const invalidUrl = "not-a-valid-url";
341 try {
342 await httpRequest(invalidUrl);
343 assert.fail("httpRequest should throw for an invalid URL");
344 } catch (err) {
345 assert.equal(err.name, "NonRetryableError");
346 assert.match(err.message, /Invalid URL: not-a-valid-url/);
347 }
348 });
349
349 lines JAVASCRIPT