返回 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 previousTui = process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
34 const previousLegacy = process.env.DEEPSEEK_FORCE_DOWNLOAD;
35 delete process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
36 delete process.env.DEEPSEEK_FORCE_DOWNLOAD;
37 try {
38 return await callback();
39 } finally {
40 if (previousTui === undefined) {
41 delete process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
42 } else {
43 process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD = previousTui;
44 }
45 if (previousLegacy === undefined) {
46 delete process.env.DEEPSEEK_FORCE_DOWNLOAD;
47 } else {
48 process.env.DEEPSEEK_FORCE_DOWNLOAD = previousLegacy;
49 }
50 }
51 }
52
53 test("install script checks Node support before loading helpers", () => {
54 const guardIndex = installScript.indexOf("assertSupportedNode();");
55 const firstRequireIndex = installScript.indexOf("require(");
56
57 assert.notEqual(guardIndex, -1);
58 assert.notEqual(firstRequireIndex, -1);
59 assert.ok(guardIndex < firstRequireIndex);
60 });
61
62 test("install script remains parseable before the Node support guard runs", () => {
63 assert.equal(installScript.includes("??"), false);
64 assert.equal(installScript.includes("?."), false);
65 });
66
67 test("native shortcut has its own platform asset and installed path", () => {
68 const paths = _internal.binaryPaths();
69 assert.match(paths.codew.asset, /^codew-/);
70 assert.equal(path.basename(paths.codew.target), process.platform === "win32" ? "codew.exe" : "codew");
71 assert.notEqual(paths.codew.target, paths.codewhale.target);
72 });
73
74 test("install failure hint explains release base override for blocked GitHub downloads", () => {
75 const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
76 delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
77 try {
78 const error = Object.assign(
79 new Error(
80 "fetch https://github.com/Hmbown/CodeWhale/releases/download/v0.8.19/codewhale-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
81 ),
82 { code: "ENOTFOUND" },
83 );
84
85 const hint = installFailureHint(error);
86
87 assert.match(hint, /DEEPSEEK_TUI_RELEASE_BASE_URL/);
88 assert.match(hint, /codewhale-artifacts-sha256\.txt/);
89 assert.match(hint, /platform binaries/);
90 assert.match(hint, /#npm-binary-download-times-out/);
91 } finally {
92 if (previous === undefined) {
93 delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
94 } else {
95 process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
96 }
97 }
98 });
99
100 test("install failure hint checks configured release base when override is already set", () => {
101 const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
102 process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = "https://mirror.example/deepseek/";
103 try {
104 const error = Object.assign(new Error("download stalled"), {
105 code: "EDOWNLOADTIMEOUT",
106 });
107
108 const hint = installFailureHint(error);
109
110 assert.match(hint, /is set to https:\/\/mirror\.example\/deepseek\//);
111 assert.match(hint, /codewhale-artifacts-sha256\.txt/);
112 assert.doesNotMatch(hint, /If GitHub is unavailable/);
113 } finally {
114 if (previous === undefined) {
115 delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
116 } else {
117 process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
118 }
119 }
120 });
121
122 test("glibc preflight message is Codewhale-branded and actionable", () => {
123 const message = glibcInternal.glibcCompatibilityMessage([2, 39, 0], [2, 35, 0]);
124
125 assert.match(message, /Prebuilt Codewhale Linux binaries require GLIBC_2\.39/);
126 assert.match(message, /this system has glibc 2\.35/);
127 assert.match(message, /cargo install codewhale-cli --locked/);
128 assert.match(message, /Linux x64 release asset is a static \(musl\) build/);
129 assert.match(message, /Linux arm64 asset is a GNU libc build/);
130 assert.match(message, /CODEWHALE_SKIP_GLIBC_CHECK=1/);
131 });
132
133 test("glibc preflight accepts canonical and legacy skip env vars", () => {
134 const previousCodewhale = process.env.CODEWHALE_SKIP_GLIBC_CHECK;
135 const previousTui = process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK;
136 const previousLegacy = process.env.DEEPSEEK_SKIP_GLIBC_CHECK;
137 delete process.env.CODEWHALE_SKIP_GLIBC_CHECK;
138 delete process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK;
139 delete process.env.DEEPSEEK_SKIP_GLIBC_CHECK;
140 try {
141 assert.equal(glibcInternal.skipGlibcCheck(), false);
142 process.env.CODEWHALE_SKIP_GLIBC_CHECK = "1";
143 assert.equal(glibcInternal.skipGlibcCheck(), true);
144 delete process.env.CODEWHALE_SKIP_GLIBC_CHECK;
145 process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK = "1";
146 assert.equal(glibcInternal.skipGlibcCheck(), true);
147 } finally {
148 if (previousCodewhale === undefined) {
149 delete process.env.CODEWHALE_SKIP_GLIBC_CHECK;
150 } else {
151 process.env.CODEWHALE_SKIP_GLIBC_CHECK = previousCodewhale;
152 }
153 if (previousTui === undefined) {
154 delete process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK;
155 } else {
156 process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK = previousTui;
157 }
158 if (previousLegacy === undefined) {
159 delete process.env.DEEPSEEK_SKIP_GLIBC_CHECK;
160 } else {
161 process.env.DEEPSEEK_SKIP_GLIBC_CHECK = previousLegacy;
162 }
163 }
164 });
165
166 test("ensureBinary adopts a manually placed target binary after checksum validation", async (t) => {
167 const dir = await makeTempDir(t);
168 const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
169 const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
170 const version = "0.8.25";
171 const content = Buffer.from("manual codewhale binary");
172 let checksumLoads = 0;
173
174 await fs.promises.writeFile(target, content, { mode: 0o600 });
175 await fs.promises.writeFile(`${target}.version`, "0.8.24", "utf8");
176
177 const result = await withoutForcedDownload(() =>
178 _internal.ensureBinary(target, assetName, version, "Hmbown/CodeWhale", async () => {
179 checksumLoads += 1;
180 return new Map([[assetName, sha256(content)]]);
181 }),
182 );
183
184 assert.equal(result, target);
185 assert.equal(checksumLoads, 1);
186 assert.equal(await fs.promises.readFile(`${target}.version`, "utf8"), version);
187 if (process.platform !== "win32") {
188 assert.notEqual((await fs.promises.stat(target)).mode & 0o111, 0);
189 }
190 });
191
192 test("ensureBinary adopts an official release-named binary placed in downloads", async (t) => {
193 const dir = await makeTempDir(t);
194 const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
195 const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
196 const assetPath = path.join(dir, assetName);
197 const version = "0.8.25";
198 const content = Buffer.from("official release binary");
199
200 await fs.promises.writeFile(assetPath, content);
201
202 const result = await withoutForcedDownload(() =>
203 _internal.ensureBinary(target, assetName, version, "Hmbown/CodeWhale", async () =>
204 new Map([[assetName, sha256(content)]]),
205 ),
206 );
207
208 assert.equal(result, target);
209 assert.equal(await exists(target), true);
210 assert.equal(await exists(assetPath), false);
211 assert.equal(await fs.promises.readFile(`${target}.version`, "utf8"), version);
212 });
213
214 test("manual binaries with mismatched checksums are not adopted", async (t) => {
215 const dir = await makeTempDir(t);
216 const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
217 const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
218 const content = Buffer.from("wrong binary bytes");
219
220 await fs.promises.writeFile(target, content);
221
222 const adopted = await _internal.adoptExistingBinaryIfValid(
223 target,
224 assetName,
225 "0.8.25",
226 async () => new Map([[assetName, sha256(Buffer.from("different bytes"))]]),
227 `${target}.version`,
228 );
229
230 assert.equal(adopted, false);
231 assert.equal(await exists(`${target}.version`), false);
232 });
233
234 test("resolvePackageVersion honors codewhaleBinaryVersion precedence (#3769)", () => {
235 const { resolvePackageVersion } = _internal;
236
237 // codewhaleBinaryVersion wins over deepseekBinaryVersion and pkg.version.
238 assert.equal(
239 resolvePackageVersion(
240 {
241 codewhaleBinaryVersion: "1.2.3",
242 deepseekBinaryVersion: "0.0.1",
243 version: "9.9.9",
244 },
245 {},
246 ),
247 "1.2.3",
248 );
249
250 // Falls back to deepseekBinaryVersion, then pkg.version.
251 assert.equal(
252 resolvePackageVersion({ deepseekBinaryVersion: "0.0.1", version: "9.9.9" }, {}),
253 "0.0.1",
254 );
255 assert.equal(resolvePackageVersion({ version: "9.9.9" }, {}), "9.9.9");
256
257 // Legacy env vars still take precedence over package fields, unchanged.
258 assert.equal(
259 resolvePackageVersion(
260 { codewhaleBinaryVersion: "1.2.3", version: "9.9.9" },
261 { DEEPSEEK_TUI_VERSION: "7.7.7" },
262 ),
263 "7.7.7",
264 );
265 assert.equal(
266 resolvePackageVersion(
267 { codewhaleBinaryVersion: "1.2.3" },
268 { DEEPSEEK_VERSION: "8.8.8" },
269 ),
270 "8.8.8",
271 );
272 });
273
274 test("httpRequest handles invalid URL parsing errors", async () => {
275 const { httpRequest } = _internal;
276 const invalidUrl = "not-a-valid-url";
277 try {
278 await httpRequest(invalidUrl);
279 assert.fail("httpRequest should throw for an invalid URL");
280 } catch (err) {
281 assert.equal(err.name, "NonRetryableError");
282 assert.match(err.message, /Invalid URL: not-a-valid-url/);
283 }
284 });
285
285 lines JAVASCRIPT