返回 DeepSeek-Reasonix
release-channels.test.mjs
根目录 / site / src / scripts / release-channels.test.mjs
1 import { test } from "node:test";
2 import assert from "node:assert/strict";
3 import { readFile } from "node:fs/promises";
4 import { desktopDownloadVersion } from "../data/desktop-download.js";
5 import {
6 CLI_RELEASE_ASSETS,
7 cliUpgradeCommand,
8 cliReleaseModel,
9 desktopGitHubReleaseModel,
10 desktopReleaseModel,
11 fetchDesktopDownloadModel,
12 fetchFirstJSON,
13 releaseAssetMap,
14 releaseVersionLabel,
15 selectCLIRelease,
16 } from "./release-channels.js";
17
18 function cliAssets(tag, missing = []) {
19 const skip = new Set(missing);
20 return CLI_RELEASE_ASSETS.filter((name) => !skip.has(name)).map((name) => ({
21 name,
22 browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
23 size: 42,
24 }));
25 }
26
27 const desktopSHA256 = "a".repeat(64);
28
29 // Every approved manual tag is probed. Selection must stay "newest that
30 // actually resolves", so listing the next tag early cannot downgrade the page.
31 const manualTagOf = (url) => (url.match(/desktop-v\d+\.\d+\.\d+/) || [])[0];
32
33 test("production Desktop downloads follow the current Stable manifest", () => {
34 assert.equal(desktopDownloadVersion, "");
35 });
36
37 test("a website pin requests only the exact Desktop version", async () => {
38 const requests = [];
39 const model = await fetchDesktopDownloadModel(async (url) => {
40 requests.push(url);
41 return { ok: true, json: async () => desktopManifest("v1.38.3") };
42 }, "v1.38.3");
43 assert.equal(model.version, "v1.38.3");
44 assert.deepEqual(requests, ["https://dl.reasonix.io/desktop-v1.38.3/latest.json"]);
45 assert.ok(Object.values(model.assets).every((url) => url.includes("/desktop-v1.38.3/")));
46 });
47
48 test("a website pin rejects mismatched manifests and falls back to the exact GitHub release", async () => {
49 const requests = [];
50 const model = await fetchDesktopDownloadModel(async (url) => {
51 requests.push(url);
52 return { ok: true, json: async () => url.includes("api.github.com")
53 ? desktopGitHubRelease("v1.38.3") : desktopManifest("v1.38.9") };
54 }, "v1.38.3");
55 assert.equal(model.version, "v1.38.3");
56 assert.deepEqual(requests, [
57 "https://dl.reasonix.io/desktop-v1.38.3/latest.json",
58 "https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases/tags/desktop-v1.38.3",
59 ]);
60 });
61
62 test("an unavailable or mismatched website pin never selects a newer release", async () => {
63 for (const payload of [null, desktopManifest("v1.38.9"), desktopGitHubRelease("v1.38.9")]) {
64 assert.equal(await fetchDesktopDownloadModel(async () => ({
65 ok: true, json: async () => payload,
66 }), "v1.38.3"), null);
67 }
68 assert.equal(await fetchDesktopDownloadModel(async () => { throw new Error("offline"); }, "v1.38.3"), null);
69 });
70
71 test("manual desktop downloads advance independently and yield to future stable releases", async () => {
72 for (const stableVersion of ["v1.38.7", "v1.38.8", "v1.39.0"]) {
73 const model = await fetchDesktopDownloadModel(async (url) => {
74 const tag = manualTagOf(url);
75 return { ok: true, json: async () => desktopManifest(tag ? tag.slice("desktop-".length) : stableVersion) };
76 });
77 const expected = stableVersion === "v1.39.0" ? stableVersion : "v1.38.9";
78 assert.equal(model.version, expected);
79 assert.ok(Object.values(model.assets).every((url) => url.includes(`desktop-${expected}/`)));
80 }
81 });
82
83 test("an approved manual tag that is not published yet cannot downgrade the page", async () => {
84 const model = await fetchDesktopDownloadModel(async (url) => {
85 const tag = manualTagOf(url);
86 if (tag === "desktop-v1.38.9") throw new Error("not published yet");
87 if (tag === "desktop-v1.38.8") return { ok: true, json: async () => desktopManifest("v1.38.8") };
88 return { ok: true, json: async () => desktopManifest("v1.38.7") };
89 });
90 assert.equal(model.version, "v1.38.8");
91 });
92
93 test("manual desktop downloads survive CDN failure through the exact published GitHub release", async () => {
94 const model = await fetchDesktopDownloadModel(async (url) => {
95 if (url.endsWith("/tags/desktop-v1.38.9")) {
96 return { ok: true, json: async () => desktopGitHubRelease("v1.38.9") };
97 }
98 if (url.includes("/latest/latest.json")) {
99 return { ok: true, json: async () => desktopManifest("v1.38.7") };
100 }
101 throw new Error("unavailable");
102 });
103 assert.equal(model.version, "v1.38.9");
104 });
105
106 test("invalid manual release cannot replace a validated stable download", async () => {
107 const model = await fetchDesktopDownloadModel(async (url) => ({
108 ok: true,
109 json: async () => manualTagOf(url) ? {} : desktopManifest("v1.38.7"),
110 }));
111 assert.equal(model.version, "v1.38.7");
112 assert.equal(await fetchDesktopDownloadModel(async () => { throw new Error("offline"); }), null);
113 });
114
115 function desktopManifest(version, base) {
116 const releaseBase = base || `https://dl.reasonix.io/desktop-${version}/`;
117 const asset = (name) => {
118 const url = releaseBase + name;
119 return { url, sig: `${url}.minisig`, size: 42, sha256: desktopSHA256 };
120 };
121 return {
122 version,
123 download_page: "https://reasonix.io/?download=desktop#start",
124 platforms: {
125 "darwin-arm64": asset("Reasonix-darwin-arm64.zip"),
126 "darwin-amd64": asset("Reasonix-darwin-amd64.zip"),
127 "windows-amd64": asset("Reasonix-windows-amd64-installer.exe"),
128 "windows-arm64": asset("Reasonix-windows-arm64-installer.exe"),
129 "linux-amd64": asset("Reasonix-linux-amd64.tar.gz"),
130 },
131 native_packages: {
132 "linux-amd64": asset("Reasonix-linux-amd64.deb"),
133 },
134 downloads: {
135 "Reasonix-darwin-arm64.dmg": asset("Reasonix-darwin-arm64.dmg"),
136 "Reasonix-darwin-amd64.dmg": asset("Reasonix-darwin-amd64.dmg"),
137 "Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"),
138 "Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip"),
139 },
140 };
141 }
142
143 function desktopGitHubRelease(version = "v1.17.21") {
144 const tag = `desktop-${version}`;
145 const names = [
146 "Reasonix-darwin-arm64.zip",
147 "Reasonix-darwin-amd64.zip",
148 "Reasonix-windows-amd64-installer.exe",
149 "Reasonix-windows-arm64-installer.exe",
150 "Reasonix-linux-amd64.tar.gz",
151 "Reasonix-linux-amd64.deb",
152 "Reasonix-darwin-universal.dmg",
153 "Reasonix-darwin-arm64.dmg",
154 "Reasonix-darwin-amd64.dmg",
155 "Reasonix-windows-amd64.zip",
156 ];
157 return {
158 tag_name: tag,
159 draft: false,
160 prerelease: false,
161 assets: names.map((name) => ({
162 name,
163 browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
164 size: 42,
165 })),
166 };
167 }
168
169 function copy(value) {
170 return structuredClone(value);
171 }
172
173 test("CLI always emits the official upgrade command", () => {
174 assert.equal(cliUpgradeCommand("stable"), "reasonix upgrade");
175 assert.equal(cliUpgradeCommand("preview"), "reasonix upgrade");
176 assert.equal(cliUpgradeCommand("canary"), "reasonix upgrade");
177 });
178
179 test("release labels use a complete version or a readable fallback", () => {
180 assert.equal(releaseVersionLabel({ version: "v1.17.21" }), "v1.17.21");
181 assert.equal(releaseVersionLabel({ version: "v1.18.0-preview.62" }), "v1.18.0-preview.62");
182 assert.equal(releaseVersionLabel(null), "latest");
183 });
184
185 test("site placeholders do not render the synthetic version vlatest", async () => {
186 const [home, docs, siteScript] = await Promise.all([
187 readFile(new URL("../pages/index.astro", import.meta.url), "utf8"),
188 readFile(new URL("../pages/docs.astro", import.meta.url), "utf8"),
189 readFile(new URL("./site.js", import.meta.url), "utf8"),
190 ]);
191 assert.doesNotMatch(home, /v<span (?:class="rxv"|data-release-version=)/);
192 assert.doesNotMatch(docs, /v<span data-release-version=/);
193 assert.doesNotMatch(home, /<a[^>]*data-cli-asset=[^>]*\sdownload(?:[ >])/);
194 assert.match(home, /id="download-tab-desktop"[^>]+aria-controls="download-pane-desktop"/);
195 assert.match(home, /id="download-pane-cli"[^>]+role="tabpanel"[^>]+hidden/);
196 assert.doesNotMatch(home, /releases\/latest\/download/);
197 assert.doesNotMatch(siteScript, /releases\/latest\/download/);
198 assert.doesNotMatch(siteScript, /desktopPreviewBase/);
199 });
200
201 test("CLI strictly excludes foreign and all prereleases", () => {
202 const releases = [
203 { tag_name: "v1.19.0-rc.1", prerelease: true, assets: cliAssets("v1.19.0-rc.1") },
204 { tag_name: "v1.18.0-preview.2", prerelease: true, assets: cliAssets("v1.18.0-preview.2") },
205 { tag_name: "desktop-v1.18.0", prerelease: false, assets: cliAssets("desktop-v1.18.0") },
206 { tag_name: "v1.17.21", prerelease: false, assets: cliAssets("v1.17.21") },
207 { tag_name: "v1.18.0-preview.12", prerelease: true, assets: cliAssets("v1.18.0-preview.12") },
208 { tag_name: "v1.18.0-preview.13", prerelease: false, assets: cliAssets("v1.18.0-preview.13") },
209 ];
210 assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.17.21");
211 assert.equal(selectCLIRelease(releases, "preview")?.tag_name, "v1.17.21");
212 });
213
214 test("CLI selection rejects incomplete releases instead of synthesizing asset URLs", () => {
215 const releases = [
216 {
217 tag_name: "v1.20.0",
218 prerelease: false,
219 assets: cliAssets("v1.20.0", ["reasonix-windows-arm64.zip", "SHA256SUMS"]),
220 },
221 {
222 tag_name: "v1.19.5",
223 prerelease: false,
224 assets: cliAssets("v1.19.5"),
225 },
226 ];
227 assert.equal(releaseAssetMap(releases[0]), null);
228 assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.19.5");
229 const model = cliReleaseModel(releases, "stable");
230 assert.equal(model?.displayVersion, "1.19.5");
231 assert.equal(
232 model?.assets["reasonix-windows-arm64.zip"],
233 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.5/reasonix-windows-arm64.zip",
234 );
235 assert.equal(cliReleaseModel([releases[0]], "stable"), null);
236 });
237
238 test("CLI selection compares arbitrarily large numeric version components exactly", () => {
239 const releases = [
240 {
241 tag_name: "v99999999999999999999.999.999",
242 prerelease: false,
243 assets: cliAssets("v99999999999999999999.999.999"),
244 },
245 {
246 tag_name: "v100000000000000000000.0.0",
247 prerelease: false,
248 assets: cliAssets("v100000000000000000000.0.0"),
249 },
250 ];
251 assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v100000000000000000000.0.0");
252 });
253
254 test("CLI release links are derived from the validated canonical tag", () => {
255 const release = {
256 tag_name: "v1.18.0",
257 prerelease: false,
258 html_url: "https://evil.invalid/phishing",
259 assets: cliAssets("v1.18.0"),
260 };
261 assert.equal(
262 cliReleaseModel([release], "stable")?.releaseURL,
263 "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.18.0",
264 );
265 assert.equal(
266 cliReleaseModel([release], "stable")?.changelogURL,
267 "https://reasonix.io/changelog/",
268 );
269 release.release_notes_url = "https://reasonix.io/changelog/v1.18.0/";
270 assert.equal(cliReleaseModel([release], "stable")?.changelogURL, release.release_notes_url);
271 });
272
273 test("CLI assets reject spoofed hosts and cross-tag URLs", () => {
274 const valid = {
275 tag_name: "v1.18.0",
276 prerelease: false,
277 assets: cliAssets("v1.18.0"),
278 };
279 const invalidURLs = [
280 "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
281 "https://github.com@evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
282 "http://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
283 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.0/reasonix-darwin-amd64.tar.gz",
284 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-arm64.tar.gz",
285 ];
286 const invalid = invalidURLs.map((browser_download_url) => {
287 const release = {
288 tag_name: "v1.20.0",
289 prerelease: false,
290 assets: cliAssets("v1.20.0"),
291 };
292 release.assets[0].browser_download_url = browser_download_url;
293 return release;
294 });
295 assert.equal(selectCLIRelease([...invalid, valid], "stable")?.tag_name, "v1.18.0");
296 });
297
298 test("CLI assets require a bounded positive safe integer size", () => {
299 for (const size of [0, -1, "42", 1.5, 1073741825, Number.MAX_SAFE_INTEGER + 1, NaN, undefined]) {
300 const release = {
301 tag_name: "v1.20.0",
302 prerelease: false,
303 assets: cliAssets("v1.20.0"),
304 };
305 release.assets[0].size = size;
306 assert.equal(releaseAssetMap(release), null, `size ${String(size)} must be rejected`);
307 }
308 });
309
310 test("CLI releases reject duplicate required asset names", () => {
311 const release = {
312 tag_name: "v1.20.0",
313 prerelease: false,
314 assets: cliAssets("v1.20.0"),
315 };
316 release.assets.push({ ...release.assets[0] });
317 assert.equal(releaseAssetMap(release), null);
318 });
319
320 test("Desktop manifests accept only official versions and old or unified asset bases", () => {
321 const preview = desktopManifest("v1.18.0-preview.62");
322 assert.equal(desktopReleaseModel(preview, "stable"), null);
323 assert.equal(desktopReleaseModel(preview, "preview"), null);
324
325 const stableR2 = desktopManifest("v1.18.0");
326 assert.equal(desktopReleaseModel(stableR2, "stable")?.version, "v1.18.0");
327 const githubBase =
328 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/";
329 const stableGitHub = desktopManifest("v1.18.0", githubBase);
330 assert.equal(
331 desktopReleaseModel(stableGitHub, "stable")?.assets["Reasonix-linux-amd64.deb"],
332 `${githubBase}Reasonix-linux-amd64.deb`,
333 );
334 const unifiedBase = "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.18.0/";
335 assert.equal(desktopReleaseModel(desktopManifest("v1.18.0", unifiedBase))?.assets["Reasonix-linux-amd64.deb"], `${unifiedBase}Reasonix-linux-amd64.deb`);
336 });
337
338 test("Desktop manifests accept historical two-download metadata and reject partial architecture DMGs", () => {
339 const historical = desktopManifest("v1.17.21");
340 delete historical.downloads["Reasonix-darwin-arm64.dmg"];
341 delete historical.downloads["Reasonix-darwin-amd64.dmg"];
342 assert.equal(desktopReleaseModel(historical)?.version, "v1.17.21");
343
344 const partial = desktopManifest("v1.39.0");
345 delete partial.downloads["Reasonix-darwin-amd64.dmg"];
346 assert.equal(desktopReleaseModel(partial), null);
347 });
348
349 test("Desktop manifests reject hostile URLs and incomplete integrity metadata", () => {
350 const cases = [
351 ["malicious host", (manifest) => {
352 const url = "https://evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
353 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
354 }],
355 ["userinfo", (manifest) => {
356 const url = "https://dl.reasonix.io@evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
357 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
358 }],
359 ["http", (manifest) => {
360 const url = "http://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
361 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
362 }],
363 ["wrong channel path", (manifest) => {
364 const url = "https://dl.reasonix.io/preview/Reasonix-darwin-arm64.zip";
365 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
366 }],
367 ["wrong filename", (manifest) => {
368 const url = "https://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-amd64.zip";
369 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
370 }],
371 ["missing platform", (manifest) => {
372 delete manifest.platforms["windows-arm64"];
373 }],
374 ["missing website download", (manifest) => {
375 delete manifest.downloads["Reasonix-darwin-universal.dmg"];
376 }],
377 ["invalid website download", (manifest) => {
378 manifest.downloads["Reasonix-windows-amd64.zip"].size = 0;
379 }],
380 ["bad SHA", (manifest) => {
381 manifest.platforms["darwin-arm64"].sha256 = "A".repeat(64);
382 }],
383 ["zero size", (manifest) => {
384 manifest.platforms["darwin-arm64"].size = 0;
385 }],
386 ["size above release maximum", (manifest) => {
387 manifest.platforms["darwin-arm64"].size = 1073741825;
388 }],
389 ["string size", (manifest) => {
390 manifest.platforms["darwin-arm64"].size = "42";
391 }],
392 ["missing size", (manifest) => {
393 delete manifest.platforms["darwin-arm64"].size;
394 }],
395 ["bad signature URL", (manifest) => {
396 manifest.platforms["darwin-arm64"].sig += "?mirror=1";
397 }],
398 ["wrong download page", (manifest) => {
399 manifest.download_page = "https://evil.invalid/download";
400 }],
401 ];
402
403 for (const [name, mutate] of cases) {
404 const manifest = copy(desktopManifest("v1.18.0-preview.62"));
405 mutate(manifest);
406 assert.equal(desktopReleaseModel(manifest, "preview"), null, name);
407 }
408
409 const crossVersion = desktopManifest(
410 "v1.18.0",
411 "https://dl.reasonix.io/desktop-v1.17.9/",
412 );
413 assert.equal(desktopReleaseModel(crossVersion, "stable"), null);
414 });
415
416 test("Desktop Stable falls back to a complete exact GitHub Latest release", () => {
417 const release = desktopGitHubRelease();
418 const model = desktopGitHubReleaseModel(release);
419 assert.equal(model?.version, "v1.17.21");
420 assert.equal(
421 model?.assets["Reasonix-darwin-universal.dmg"],
422 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-universal.dmg",
423 );
424
425 const invalid = [
426 { ...release, prerelease: true },
427 { ...release, tag_name: "v1.17.21" },
428 { ...release, assets: release.assets.slice(1) },
429 { ...release, assets: [...release.assets, { ...release.assets[0] }] },
430 ];
431 const spoofed = copy(release);
432 spoofed.assets[0].browser_download_url =
433 "https://github.com.evil.invalid/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-arm64.zip";
434 invalid.push(spoofed);
435 for (const candidate of invalid) assert.equal(desktopGitHubReleaseModel(candidate), null);
436 });
437
438 test("release JSON fetch falls back in order", async () => {
439 const calls = [];
440 const result = await fetchFirstJSON(["https://one.invalid", "https://two.invalid"], async (url) => {
441 calls.push(url);
442 if (url.includes("one")) return { ok: false, status: 503, statusText: "Unavailable" };
443 return { ok: true, json: async () => ({ version: "v1.2.3" }) };
444 });
445 assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
446 assert.deepEqual(result, { version: "v1.2.3" });
447 });
448
449 test("release JSON fetch skips a successful response that fails channel validation", async () => {
450 const calls = [];
451 const result = await fetchFirstJSON(
452 ["https://one.invalid", "https://two.invalid"],
453 async (url) => {
454 calls.push(url);
455 return {
456 ok: true,
457 json: async () => ({ version: url.includes("one") ? "v1.2.3-canary.1" : "v1.2.3-preview.1" }),
458 };
459 },
460 (payload) => payload.version === "v1.2.3-preview.1",
461 );
462 assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
463 assert.deepEqual(result, { version: "v1.2.3-preview.1" });
464 });
465
466 test("Desktop JSON fetch continues after an invalid 200 response", async () => {
467 const invalid = desktopManifest("v1.18.0");
468 invalid.platforms["darwin-arm64"].size = 0;
469 const valid = desktopManifest("v1.17.21");
470 const calls = [];
471 const result = await fetchFirstJSON(
472 ["https://one.invalid", "https://two.invalid"],
473 async (url) => {
474 calls.push(url);
475 return { ok: true, json: async () => url.includes("one") ? invalid : valid };
476 },
477 (payload) => Boolean(desktopReleaseModel(payload)),
478 );
479 assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
480 assert.equal(result.version, "v1.17.21");
481 });
482
482 lines Plain Text