返回 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 {
5 CLI_RELEASE_ASSETS,
6 cliUpgradeCommand,
7 cliReleaseModel,
8 desktopGitHubReleaseModel,
9 desktopReleaseModel,
10 fetchFirstJSON,
11 releaseAssetMap,
12 releaseVersionLabel,
13 selectCLIRelease,
14 } from "./release-channels.js";
15
16 function cliAssets(tag, missing = []) {
17 const skip = new Set(missing);
18 return CLI_RELEASE_ASSETS.filter((name) => !skip.has(name)).map((name) => ({
19 name,
20 browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
21 size: 42,
22 }));
23 }
24
25 const desktopSHA256 = "a".repeat(64);
26
27 function desktopManifest(version, base) {
28 const releaseBase = base || `https://dl.reasonix.io/desktop-${version}/`;
29 const asset = (name) => {
30 const url = releaseBase + name;
31 return { url, sig: `${url}.minisig`, size: 42, sha256: desktopSHA256 };
32 };
33 return {
34 version,
35 download_page: "https://reasonix.io/?download=desktop#start",
36 platforms: {
37 "darwin-arm64": asset("Reasonix-darwin-arm64.zip"),
38 "darwin-amd64": asset("Reasonix-darwin-amd64.zip"),
39 "windows-amd64": asset("Reasonix-windows-amd64-installer.exe"),
40 "windows-arm64": asset("Reasonix-windows-arm64-installer.exe"),
41 "linux-amd64": asset("Reasonix-linux-amd64.tar.gz"),
42 },
43 native_packages: {
44 "linux-amd64": asset("Reasonix-linux-amd64.deb"),
45 },
46 downloads: {
47 "Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"),
48 "Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip"),
49 },
50 };
51 }
52
53 function desktopGitHubRelease(version = "v1.17.21") {
54 const tag = `desktop-${version}`;
55 const names = [
56 "Reasonix-darwin-arm64.zip",
57 "Reasonix-darwin-amd64.zip",
58 "Reasonix-windows-amd64-installer.exe",
59 "Reasonix-windows-arm64-installer.exe",
60 "Reasonix-linux-amd64.tar.gz",
61 "Reasonix-linux-amd64.deb",
62 "Reasonix-darwin-universal.dmg",
63 "Reasonix-windows-amd64.zip",
64 ];
65 return {
66 tag_name: tag,
67 draft: false,
68 prerelease: false,
69 assets: names.map((name) => ({
70 name,
71 browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
72 size: 42,
73 })),
74 };
75 }
76
77 function copy(value) {
78 return structuredClone(value);
79 }
80
81 test("CLI always emits the official upgrade command", () => {
82 assert.equal(cliUpgradeCommand("stable"), "reasonix upgrade");
83 assert.equal(cliUpgradeCommand("preview"), "reasonix upgrade");
84 assert.equal(cliUpgradeCommand("canary"), "reasonix upgrade");
85 });
86
87 test("release labels use a complete version or a readable fallback", () => {
88 assert.equal(releaseVersionLabel({ version: "v1.17.21" }), "v1.17.21");
89 assert.equal(releaseVersionLabel({ version: "v1.18.0-preview.62" }), "v1.18.0-preview.62");
90 assert.equal(releaseVersionLabel(null), "latest");
91 });
92
93 test("site placeholders do not render the synthetic version vlatest", async () => {
94 const [home, docs, siteScript] = await Promise.all([
95 readFile(new URL("../pages/index.astro", import.meta.url), "utf8"),
96 readFile(new URL("../pages/docs.astro", import.meta.url), "utf8"),
97 readFile(new URL("./site.js", import.meta.url), "utf8"),
98 ]);
99 assert.doesNotMatch(home, /v<span (?:class="rxv"|data-release-version=)/);
100 assert.doesNotMatch(docs, /v<span data-release-version=/);
101 assert.doesNotMatch(home, /<a[^>]*data-cli-asset=[^>]*\sdownload(?:[ >])/);
102 assert.match(home, /id="download-tab-desktop"[^>]+aria-controls="download-pane-desktop"/);
103 assert.match(home, /id="download-pane-cli"[^>]+role="tabpanel"[^>]+hidden/);
104 assert.doesNotMatch(home, /releases\/latest\/download/);
105 assert.doesNotMatch(siteScript, /releases\/latest\/download/);
106 assert.doesNotMatch(siteScript, /desktopPreviewBase/);
107 });
108
109 test("CLI strictly excludes foreign and all prereleases", () => {
110 const releases = [
111 { tag_name: "v1.19.0-rc.1", prerelease: true, assets: cliAssets("v1.19.0-rc.1") },
112 { tag_name: "v1.18.0-preview.2", prerelease: true, assets: cliAssets("v1.18.0-preview.2") },
113 { tag_name: "desktop-v1.18.0", prerelease: false, assets: cliAssets("desktop-v1.18.0") },
114 { tag_name: "v1.17.21", prerelease: false, assets: cliAssets("v1.17.21") },
115 { tag_name: "v1.18.0-preview.12", prerelease: true, assets: cliAssets("v1.18.0-preview.12") },
116 { tag_name: "v1.18.0-preview.13", prerelease: false, assets: cliAssets("v1.18.0-preview.13") },
117 ];
118 assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.17.21");
119 assert.equal(selectCLIRelease(releases, "preview")?.tag_name, "v1.17.21");
120 });
121
122 test("CLI selection rejects incomplete releases instead of synthesizing asset URLs", () => {
123 const releases = [
124 {
125 tag_name: "v1.20.0",
126 prerelease: false,
127 assets: cliAssets("v1.20.0", ["reasonix-windows-arm64.zip", "SHA256SUMS"]),
128 },
129 {
130 tag_name: "v1.19.5",
131 prerelease: false,
132 assets: cliAssets("v1.19.5"),
133 },
134 ];
135 assert.equal(releaseAssetMap(releases[0]), null);
136 assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.19.5");
137 const model = cliReleaseModel(releases, "stable");
138 assert.equal(model?.displayVersion, "1.19.5");
139 assert.equal(
140 model?.assets["reasonix-windows-arm64.zip"],
141 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.5/reasonix-windows-arm64.zip",
142 );
143 assert.equal(cliReleaseModel([releases[0]], "stable"), null);
144 });
145
146 test("CLI selection compares arbitrarily large numeric version components exactly", () => {
147 const releases = [
148 {
149 tag_name: "v99999999999999999999.999.999",
150 prerelease: false,
151 assets: cliAssets("v99999999999999999999.999.999"),
152 },
153 {
154 tag_name: "v100000000000000000000.0.0",
155 prerelease: false,
156 assets: cliAssets("v100000000000000000000.0.0"),
157 },
158 ];
159 assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v100000000000000000000.0.0");
160 });
161
162 test("CLI release links are derived from the validated canonical tag", () => {
163 const release = {
164 tag_name: "v1.18.0",
165 prerelease: false,
166 html_url: "https://evil.invalid/phishing",
167 assets: cliAssets("v1.18.0"),
168 };
169 assert.equal(
170 cliReleaseModel([release], "stable")?.releaseURL,
171 "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.18.0",
172 );
173 assert.equal(
174 cliReleaseModel([release], "stable")?.changelogURL,
175 "https://reasonix.io/changelog/",
176 );
177 release.release_notes_url = "https://reasonix.io/changelog/v1.18.0/";
178 assert.equal(cliReleaseModel([release], "stable")?.changelogURL, release.release_notes_url);
179 });
180
181 test("CLI assets reject spoofed hosts and cross-tag URLs", () => {
182 const valid = {
183 tag_name: "v1.18.0",
184 prerelease: false,
185 assets: cliAssets("v1.18.0"),
186 };
187 const invalidURLs = [
188 "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
189 "https://github.com@evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
190 "http://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
191 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.0/reasonix-darwin-amd64.tar.gz",
192 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-arm64.tar.gz",
193 ];
194 const invalid = invalidURLs.map((browser_download_url) => {
195 const release = {
196 tag_name: "v1.20.0",
197 prerelease: false,
198 assets: cliAssets("v1.20.0"),
199 };
200 release.assets[0].browser_download_url = browser_download_url;
201 return release;
202 });
203 assert.equal(selectCLIRelease([...invalid, valid], "stable")?.tag_name, "v1.18.0");
204 });
205
206 test("CLI assets require a bounded positive safe integer size", () => {
207 for (const size of [0, -1, "42", 1.5, 1073741825, Number.MAX_SAFE_INTEGER + 1, NaN, undefined]) {
208 const release = {
209 tag_name: "v1.20.0",
210 prerelease: false,
211 assets: cliAssets("v1.20.0"),
212 };
213 release.assets[0].size = size;
214 assert.equal(releaseAssetMap(release), null, `size ${String(size)} must be rejected`);
215 }
216 });
217
218 test("CLI releases reject duplicate required asset names", () => {
219 const release = {
220 tag_name: "v1.20.0",
221 prerelease: false,
222 assets: cliAssets("v1.20.0"),
223 };
224 release.assets.push({ ...release.assets[0] });
225 assert.equal(releaseAssetMap(release), null);
226 });
227
228 test("Desktop manifests accept only official versions and old or unified asset bases", () => {
229 const preview = desktopManifest("v1.18.0-preview.62");
230 assert.equal(desktopReleaseModel(preview, "stable"), null);
231 assert.equal(desktopReleaseModel(preview, "preview"), null);
232
233 const stableR2 = desktopManifest("v1.18.0");
234 assert.equal(desktopReleaseModel(stableR2, "stable")?.version, "v1.18.0");
235 const githubBase =
236 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/";
237 const stableGitHub = desktopManifest("v1.18.0", githubBase);
238 assert.equal(
239 desktopReleaseModel(stableGitHub, "stable")?.assets["Reasonix-linux-amd64.deb"],
240 `${githubBase}Reasonix-linux-amd64.deb`,
241 );
242 const unifiedBase = "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.18.0/";
243 assert.equal(desktopReleaseModel(desktopManifest("v1.18.0", unifiedBase))?.assets["Reasonix-linux-amd64.deb"], `${unifiedBase}Reasonix-linux-amd64.deb`);
244 });
245
246 test("Desktop manifests reject hostile URLs and incomplete integrity metadata", () => {
247 const cases = [
248 ["malicious host", (manifest) => {
249 const url = "https://evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
250 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
251 }],
252 ["userinfo", (manifest) => {
253 const url = "https://dl.reasonix.io@evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
254 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
255 }],
256 ["http", (manifest) => {
257 const url = "http://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
258 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
259 }],
260 ["wrong channel path", (manifest) => {
261 const url = "https://dl.reasonix.io/preview/Reasonix-darwin-arm64.zip";
262 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
263 }],
264 ["wrong filename", (manifest) => {
265 const url = "https://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-amd64.zip";
266 Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
267 }],
268 ["missing platform", (manifest) => {
269 delete manifest.platforms["windows-arm64"];
270 }],
271 ["missing website download", (manifest) => {
272 delete manifest.downloads["Reasonix-darwin-universal.dmg"];
273 }],
274 ["invalid website download", (manifest) => {
275 manifest.downloads["Reasonix-windows-amd64.zip"].size = 0;
276 }],
277 ["bad SHA", (manifest) => {
278 manifest.platforms["darwin-arm64"].sha256 = "A".repeat(64);
279 }],
280 ["zero size", (manifest) => {
281 manifest.platforms["darwin-arm64"].size = 0;
282 }],
283 ["size above release maximum", (manifest) => {
284 manifest.platforms["darwin-arm64"].size = 1073741825;
285 }],
286 ["string size", (manifest) => {
287 manifest.platforms["darwin-arm64"].size = "42";
288 }],
289 ["missing size", (manifest) => {
290 delete manifest.platforms["darwin-arm64"].size;
291 }],
292 ["bad signature URL", (manifest) => {
293 manifest.platforms["darwin-arm64"].sig += "?mirror=1";
294 }],
295 ["wrong download page", (manifest) => {
296 manifest.download_page = "https://evil.invalid/download";
297 }],
298 ];
299
300 for (const [name, mutate] of cases) {
301 const manifest = copy(desktopManifest("v1.18.0-preview.62"));
302 mutate(manifest);
303 assert.equal(desktopReleaseModel(manifest, "preview"), null, name);
304 }
305
306 const crossVersion = desktopManifest(
307 "v1.18.0",
308 "https://dl.reasonix.io/desktop-v1.17.9/",
309 );
310 assert.equal(desktopReleaseModel(crossVersion, "stable"), null);
311 });
312
313 test("Desktop Stable falls back to a complete exact GitHub Latest release", () => {
314 const release = desktopGitHubRelease();
315 const model = desktopGitHubReleaseModel(release);
316 assert.equal(model?.version, "v1.17.21");
317 assert.equal(
318 model?.assets["Reasonix-darwin-universal.dmg"],
319 "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-universal.dmg",
320 );
321
322 const invalid = [
323 { ...release, prerelease: true },
324 { ...release, tag_name: "v1.17.21" },
325 { ...release, assets: release.assets.slice(1) },
326 { ...release, assets: [...release.assets, { ...release.assets[0] }] },
327 ];
328 const spoofed = copy(release);
329 spoofed.assets[0].browser_download_url =
330 "https://github.com.evil.invalid/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-arm64.zip";
331 invalid.push(spoofed);
332 for (const candidate of invalid) assert.equal(desktopGitHubReleaseModel(candidate), null);
333 });
334
335 test("release JSON fetch falls back in order", async () => {
336 const calls = [];
337 const result = await fetchFirstJSON(["https://one.invalid", "https://two.invalid"], async (url) => {
338 calls.push(url);
339 if (url.includes("one")) return { ok: false, status: 503, statusText: "Unavailable" };
340 return { ok: true, json: async () => ({ version: "v1.2.3" }) };
341 });
342 assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
343 assert.deepEqual(result, { version: "v1.2.3" });
344 });
345
346 test("release JSON fetch skips a successful response that fails channel validation", async () => {
347 const calls = [];
348 const result = await fetchFirstJSON(
349 ["https://one.invalid", "https://two.invalid"],
350 async (url) => {
351 calls.push(url);
352 return {
353 ok: true,
354 json: async () => ({ version: url.includes("one") ? "v1.2.3-canary.1" : "v1.2.3-preview.1" }),
355 };
356 },
357 (payload) => payload.version === "v1.2.3-preview.1",
358 );
359 assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
360 assert.deepEqual(result, { version: "v1.2.3-preview.1" });
361 });
362
363 test("Desktop JSON fetch continues after an invalid 200 response", async () => {
364 const invalid = desktopManifest("v1.18.0");
365 invalid.platforms["darwin-arm64"].size = 0;
366 const valid = desktopManifest("v1.17.21");
367 const calls = [];
368 const result = await fetchFirstJSON(
369 ["https://one.invalid", "https://two.invalid"],
370 async (url) => {
371 calls.push(url);
372 return { ok: true, json: async () => url.includes("one") ? invalid : valid };
373 },
374 (payload) => Boolean(desktopReleaseModel(payload)),
375 );
376 assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
377 assert.equal(result.version, "v1.17.21");
378 });
379
379 lines Plain Text