| 1 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | import { |
| 3 | cliReleaseChannel, |
| 4 | desktopReleaseChannel, |
| 5 | handleCLIRelease, |
| 6 | handleDesktopReleaseManifest, |
| 7 | } from "./desktop_release"; |
| 8 | import worker from "./index"; |
| 9 | |
| 10 | const sha256 = "a".repeat(64); |
| 11 | |
| 12 | function desktopManifest(version: string, base?: string) { |
| 13 | const releaseBase = base ?? `https://dl.reasonix.io/desktop-${version}/`; |
| 14 | const asset = (name: string) => { |
| 15 | const url = releaseBase + name; |
| 16 | return { url, sig: `${url}.minisig`, size: 42, sha256 }; |
| 17 | }; |
| 18 | return { |
| 19 | version, |
| 20 | download_page: "https://reasonix.io/?download=desktop#start", |
| 21 | platforms: { |
| 22 | "darwin-arm64": asset("Reasonix-darwin-arm64.zip"), |
| 23 | "darwin-amd64": asset("Reasonix-darwin-amd64.zip"), |
| 24 | "windows-amd64": asset("Reasonix-windows-amd64-installer.exe"), |
| 25 | "windows-arm64": asset("Reasonix-windows-arm64-installer.exe"), |
| 26 | "linux-amd64": asset("Reasonix-linux-amd64.tar.gz"), |
| 27 | }, |
| 28 | native_packages: { |
| 29 | "linux-amd64": asset("Reasonix-linux-amd64.deb"), |
| 30 | }, |
| 31 | downloads: { |
| 32 | "Reasonix-darwin-arm64.dmg": asset("Reasonix-darwin-arm64.dmg"), |
| 33 | "Reasonix-darwin-amd64.dmg": asset("Reasonix-darwin-amd64.dmg"), |
| 34 | "Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"), |
| 35 | "Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip"), |
| 36 | }, |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | function desktopManifestText(version: string, base?: string): string { |
| 41 | return JSON.stringify(desktopManifest(version, base)); |
| 42 | } |
| 43 | |
| 44 | function githubDesktopRelease(version: string, overrides: Record<string, unknown> = {}) { |
| 45 | const tag = `desktop-${version}`; |
| 46 | return { |
| 47 | tag_name: tag, |
| 48 | draft: false, |
| 49 | prerelease: false, |
| 50 | assets: [{ |
| 51 | name: "latest.json", |
| 52 | browser_download_url: |
| 53 | `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/latest.json`, |
| 54 | size: 42, |
| 55 | }], |
| 56 | ...overrides, |
| 57 | }; |
| 58 | } |
| 59 | |
| 60 | const cliAssets = [ |
| 61 | "reasonix-darwin-amd64.tar.gz", |
| 62 | "reasonix-darwin-arm64.tar.gz", |
| 63 | "reasonix-linux-amd64.tar.gz", |
| 64 | "reasonix-linux-arm64.tar.gz", |
| 65 | "reasonix-windows-amd64.zip", |
| 66 | "reasonix-windows-arm64.zip", |
| 67 | "SHA256SUMS", |
| 68 | ]; |
| 69 | |
| 70 | const cliRelease = (tag: string, prerelease: boolean) => ({ |
| 71 | tag_name: tag, |
| 72 | prerelease, |
| 73 | html_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/tag/${tag}`, |
| 74 | assets: cliAssets.map((name) => ({ |
| 75 | name, |
| 76 | browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`, |
| 77 | size: 42, |
| 78 | })), |
| 79 | }); |
| 80 | |
| 81 | afterEach(() => { |
| 82 | vi.unstubAllGlobals(); |
| 83 | }); |
| 84 | |
| 85 | describe("desktop Preview release gateway", () => { |
| 86 | it("recognizes Preview and the legacy Canary compatibility route", () => { |
| 87 | expect(desktopReleaseChannel("/v1/desktop/releases/preview/latest.json")).toBe("preview"); |
| 88 | expect(desktopReleaseChannel("/v1/desktop/releases/canary/latest.json")).toBe("canary"); |
| 89 | expect(desktopReleaseChannel("/v1/desktop/releases/rc/latest.json")).toBeNull(); |
| 90 | }); |
| 91 | |
| 92 | it("serves a complete Preview manifest from the canonical pointer first", async () => { |
| 93 | const fetchMock = vi.fn(async (_url: string) => |
| 94 | new Response(desktopManifestText("v1.2.0-preview.7"), { status: 200 }), |
| 95 | ); |
| 96 | vi.stubGlobal("fetch", fetchMock); |
| 97 | |
| 98 | const response = await handleDesktopReleaseManifest("preview"); |
| 99 | |
| 100 | expect(response.status).toBe(200); |
| 101 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 102 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-preview"); |
| 103 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 104 | expect(fetchMock.mock.calls[0]?.[0]).toBe("https://dl.reasonix.io/preview/latest.json"); |
| 105 | }); |
| 106 | |
| 107 | it("keeps serving the signed rolling Preview manifest during pointer migration", async () => { |
| 108 | const legacy = desktopManifest( |
| 109 | "v1.18.0-preview.62", |
| 110 | "https://dl.reasonix.io/desktop-preview/", |
| 111 | ); |
| 112 | Reflect.deleteProperty(legacy, "downloads"); |
| 113 | const fetchMock = vi.fn(async () => |
| 114 | new Response(JSON.stringify(legacy), { status: 200 }), |
| 115 | ); |
| 116 | vi.stubGlobal("fetch", fetchMock); |
| 117 | |
| 118 | const response = await handleDesktopReleaseManifest("preview"); |
| 119 | |
| 120 | expect(response.status).toBe(200); |
| 121 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-preview"); |
| 122 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 123 | }); |
| 124 | |
| 125 | it("rejects a new-format Preview manifest that still uses rolling assets", async () => { |
| 126 | const rolling = desktopManifest( |
| 127 | "v1.18.0-preview.63", |
| 128 | "https://dl.reasonix.io/desktop-preview/", |
| 129 | ); |
| 130 | const fetchMock = vi |
| 131 | .fn() |
| 132 | .mockResolvedValueOnce(new Response(JSON.stringify(rolling), { status: 200 })) |
| 133 | .mockResolvedValueOnce(new Response("missing", { status: 404 })); |
| 134 | vi.stubGlobal("fetch", fetchMock); |
| 135 | |
| 136 | const response = await handleDesktopReleaseManifest("preview"); |
| 137 | |
| 138 | expect(response.status).toBe(502); |
| 139 | expect(fetchMock).toHaveBeenCalledTimes(2); |
| 140 | }); |
| 141 | |
| 142 | it("continues to the compatibility pointer after an invalid 200 response", async () => { |
| 143 | const invalid = desktopManifest("v1.2.0-preview.8"); |
| 144 | invalid.platforms["darwin-arm64"].size = 0; |
| 145 | const fetchMock = vi |
| 146 | .fn() |
| 147 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 148 | .mockResolvedValueOnce(new Response(desktopManifestText("v1.2.0-preview.7"), { status: 200 })); |
| 149 | vi.stubGlobal("fetch", fetchMock); |
| 150 | |
| 151 | const response = await handleDesktopReleaseManifest("preview"); |
| 152 | |
| 153 | expect(response.status).toBe(200); |
| 154 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-canary-compat"); |
| 155 | expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([ |
| 156 | "https://dl.reasonix.io/preview/latest.json", |
| 157 | "https://dl.reasonix.io/canary/latest.json", |
| 158 | ]); |
| 159 | }); |
| 160 | |
| 161 | it("rejects hostile URLs and incomplete Desktop manifests", async () => { |
| 162 | const cases: Array<[string, (manifest: ReturnType<typeof desktopManifest>) => void]> = [ |
| 163 | ["malicious host", (manifest) => { |
| 164 | const url = "https://evil.invalid/desktop-v1.2.0-preview.7/Reasonix-darwin-arm64.zip"; |
| 165 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 166 | }], |
| 167 | ["userinfo", (manifest) => { |
| 168 | const url = "https://dl.reasonix.io@evil.invalid/desktop-v1.2.0-preview.7/Reasonix-darwin-arm64.zip"; |
| 169 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 170 | }], |
| 171 | ["http", (manifest) => { |
| 172 | const url = "http://dl.reasonix.io/desktop-v1.2.0-preview.7/Reasonix-darwin-arm64.zip"; |
| 173 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 174 | }], |
| 175 | ["wrong channel path", (manifest) => { |
| 176 | const url = "https://dl.reasonix.io/preview/Reasonix-darwin-arm64.zip"; |
| 177 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 178 | }], |
| 179 | ["wrong filename", (manifest) => { |
| 180 | const url = "https://dl.reasonix.io/desktop-v1.2.0-preview.7/Reasonix-darwin-amd64.zip"; |
| 181 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 182 | }], |
| 183 | ["missing asset", (manifest) => { |
| 184 | delete (manifest.platforms as Partial<typeof manifest.platforms>)["windows-arm64"]; |
| 185 | }], |
| 186 | ["missing website download", (manifest) => { |
| 187 | delete (manifest.downloads as Partial<typeof manifest.downloads>)["Reasonix-darwin-universal.dmg"]; |
| 188 | }], |
| 189 | ["invalid website download", (manifest) => { |
| 190 | manifest.downloads["Reasonix-windows-amd64.zip"].size = 0; |
| 191 | }], |
| 192 | ["bad SHA", (manifest) => { |
| 193 | manifest.platforms["darwin-arm64"].sha256 = "A".repeat(64); |
| 194 | }], |
| 195 | ["zero size", (manifest) => { |
| 196 | manifest.platforms["darwin-arm64"].size = 0; |
| 197 | }], |
| 198 | ["size above release maximum", (manifest) => { |
| 199 | manifest.platforms["darwin-arm64"].size = 1073741825; |
| 200 | }], |
| 201 | ["bad signature", (manifest) => { |
| 202 | manifest.platforms["darwin-arm64"].sig += "?mirror=1"; |
| 203 | }], |
| 204 | ["wrong download page", (manifest) => { |
| 205 | manifest.download_page = "https://evil.invalid/download"; |
| 206 | }], |
| 207 | ]; |
| 208 | |
| 209 | for (const [name, mutate] of cases) { |
| 210 | const manifest = desktopManifest("v1.2.0-preview.7"); |
| 211 | mutate(manifest); |
| 212 | const fetchMock = vi |
| 213 | .fn() |
| 214 | .mockResolvedValueOnce(new Response(JSON.stringify(manifest), { status: 200 })) |
| 215 | .mockResolvedValueOnce(new Response("missing", { status: 404 })); |
| 216 | vi.stubGlobal("fetch", fetchMock); |
| 217 | |
| 218 | const response = await handleDesktopReleaseManifest("preview"); |
| 219 | |
| 220 | expect(response.status, name).toBe(502); |
| 221 | expect(fetchMock, name).toHaveBeenCalledTimes(2); |
| 222 | vi.unstubAllGlobals(); |
| 223 | } |
| 224 | }); |
| 225 | }); |
| 226 | |
| 227 | describe("desktop Stable GitHub fallback", () => { |
| 228 | it("accepts the exact versioned R2 asset directory", async () => { |
| 229 | const fetchMock = vi.fn(async (_url: string) => |
| 230 | new Response(desktopManifestText("v1.18.0"), { status: 200 }), |
| 231 | ); |
| 232 | vi.stubGlobal("fetch", fetchMock); |
| 233 | |
| 234 | const response = await handleDesktopReleaseManifest("stable"); |
| 235 | |
| 236 | expect(response.status).toBe(200); |
| 237 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-stable"); |
| 238 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 239 | }); |
| 240 | |
| 241 | it("keeps serving a legacy Stable manifest that predates website downloads", async () => { |
| 242 | const legacy = desktopManifest("v1.17.21"); |
| 243 | Reflect.deleteProperty(legacy, "downloads"); |
| 244 | const fetchMock = vi.fn(async () => |
| 245 | new Response(JSON.stringify(legacy), { status: 200 }), |
| 246 | ); |
| 247 | vi.stubGlobal("fetch", fetchMock); |
| 248 | |
| 249 | const response = await handleDesktopReleaseManifest("stable"); |
| 250 | |
| 251 | expect(response.status).toBe(200); |
| 252 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-stable"); |
| 253 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 254 | }); |
| 255 | |
| 256 | it("keeps serving the historical two-download manifest", async () => { |
| 257 | const historical = desktopManifest("v1.17.21"); |
| 258 | Reflect.deleteProperty(historical.downloads, "Reasonix-darwin-arm64.dmg"); |
| 259 | Reflect.deleteProperty(historical.downloads, "Reasonix-darwin-amd64.dmg"); |
| 260 | const fetchMock = vi.fn(async () => |
| 261 | new Response(JSON.stringify(historical), { status: 200 }), |
| 262 | ); |
| 263 | vi.stubGlobal("fetch", fetchMock); |
| 264 | |
| 265 | const response = await handleDesktopReleaseManifest("stable"); |
| 266 | expect(response.status).toBe(200); |
| 267 | }); |
| 268 | |
| 269 | it("accepts null but rejects an empty downloads object as legacy", async () => { |
| 270 | const nullDownloads = desktopManifest("v1.17.21"); |
| 271 | Reflect.set(nullDownloads, "downloads", null); |
| 272 | const nullFetch = vi.fn(async () => |
| 273 | new Response(JSON.stringify(nullDownloads), { status: 200 }), |
| 274 | ); |
| 275 | vi.stubGlobal("fetch", nullFetch); |
| 276 | |
| 277 | const legacyResponse = await handleDesktopReleaseManifest("stable"); |
| 278 | expect(legacyResponse.status).toBe(200); |
| 279 | expect(nullFetch).toHaveBeenCalledTimes(1); |
| 280 | |
| 281 | const emptyDownloads = desktopManifest("v1.17.21"); |
| 282 | Reflect.set(emptyDownloads, "downloads", {}); |
| 283 | const emptyFetch = vi |
| 284 | .fn() |
| 285 | .mockResolvedValueOnce(new Response(JSON.stringify(emptyDownloads), { status: 200 })) |
| 286 | .mockResolvedValueOnce(new Response("missing", { status: 404 })); |
| 287 | vi.stubGlobal("fetch", emptyFetch); |
| 288 | |
| 289 | const invalidResponse = await handleDesktopReleaseManifest("stable"); |
| 290 | expect(invalidResponse.status).toBe(502); |
| 291 | expect(emptyFetch).toHaveBeenCalledTimes(2); |
| 292 | }); |
| 293 | |
| 294 | it("uses /releases/latest and requires the release tag to match the manifest version", async () => { |
| 295 | const githubBase = |
| 296 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/"; |
| 297 | const invalidR2 = desktopManifest("v1.19.0"); |
| 298 | invalidR2.platforms["darwin-arm64"].size = 0; |
| 299 | const fetchMock = vi |
| 300 | .fn() |
| 301 | .mockResolvedValueOnce(new Response(JSON.stringify(invalidR2), { status: 200 })) |
| 302 | .mockResolvedValueOnce(new Response(JSON.stringify(githubDesktopRelease("v1.18.0")), { status: 200 })) |
| 303 | .mockResolvedValueOnce(new Response(desktopManifestText("v1.18.0", githubBase), { status: 200 })); |
| 304 | vi.stubGlobal("fetch", fetchMock); |
| 305 | |
| 306 | const response = await handleDesktopReleaseManifest("stable"); |
| 307 | const body = await response.json() as { version?: string }; |
| 308 | |
| 309 | expect(response.status).toBe(200); |
| 310 | expect(body.version).toBe("v1.18.0"); |
| 311 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-desktop-release"); |
| 312 | expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([ |
| 313 | "https://dl.reasonix.io/latest/latest.json", |
| 314 | "https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases/latest", |
| 315 | `${githubBase}latest.json`, |
| 316 | ]); |
| 317 | }); |
| 318 | |
| 319 | it("rejects a GitHub manifest whose version disagrees with the latest release tag", async () => { |
| 320 | const manifestBase = |
| 321 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.9/"; |
| 322 | const fetchMock = vi |
| 323 | .fn() |
| 324 | .mockResolvedValueOnce(new Response("missing", { status: 404 })) |
| 325 | .mockResolvedValueOnce(new Response(JSON.stringify(githubDesktopRelease("v1.18.0")), { status: 200 })) |
| 326 | .mockResolvedValueOnce(new Response(desktopManifestText("v1.17.9", manifestBase), { status: 200 })); |
| 327 | vi.stubGlobal("fetch", fetchMock); |
| 328 | |
| 329 | const response = await handleDesktopReleaseManifest("stable"); |
| 330 | |
| 331 | expect(response.status).toBe(502); |
| 332 | expect(fetchMock).toHaveBeenCalledTimes(3); |
| 333 | }); |
| 334 | |
| 335 | it("rejects a non-canonical or zero-byte latest.json release asset", async () => { |
| 336 | for (const asset of [ |
| 337 | { |
| 338 | name: "latest.json", |
| 339 | browser_download_url: |
| 340 | "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/latest.json", |
| 341 | size: 42, |
| 342 | }, |
| 343 | { |
| 344 | name: "latest.json", |
| 345 | browser_download_url: |
| 346 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/latest.json", |
| 347 | size: 0, |
| 348 | }, |
| 349 | ]) { |
| 350 | const release = githubDesktopRelease("v1.18.0", { assets: [asset] }); |
| 351 | const fetchMock = vi |
| 352 | .fn() |
| 353 | .mockResolvedValueOnce(new Response("missing", { status: 404 })) |
| 354 | .mockResolvedValueOnce(new Response(JSON.stringify(release), { status: 200 })); |
| 355 | vi.stubGlobal("fetch", fetchMock); |
| 356 | |
| 357 | const response = await handleDesktopReleaseManifest("stable"); |
| 358 | |
| 359 | expect(response.status).toBe(502); |
| 360 | expect(fetchMock).toHaveBeenCalledTimes(2); |
| 361 | vi.unstubAllGlobals(); |
| 362 | } |
| 363 | }); |
| 364 | }); |
| 365 | |
| 366 | describe("release gateway HTTP method contract", () => { |
| 367 | const env = {} as Parameters<typeof worker.fetch>[1]; |
| 368 | |
| 369 | it("answers CORS preflight without loading an upstream release", async () => { |
| 370 | const fetchMock = vi.fn(); |
| 371 | vi.stubGlobal("fetch", fetchMock); |
| 372 | |
| 373 | const response = await worker.fetch( |
| 374 | new Request("https://crash.reasonix.io/v1/cli/releases/stable/latest.json", { |
| 375 | method: "OPTIONS", |
| 376 | }), |
| 377 | env, |
| 378 | ); |
| 379 | |
| 380 | expect(response.status).toBe(204); |
| 381 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 382 | expect(response.headers.get("access-control-allow-methods")).toBe("GET, HEAD, OPTIONS"); |
| 383 | expect(response.headers.get("access-control-max-age")).toBe("86400"); |
| 384 | expect(response.headers.get("allow")).toBe("GET, HEAD, OPTIONS"); |
| 385 | expect(fetchMock).not.toHaveBeenCalled(); |
| 386 | }); |
| 387 | |
| 388 | it("serves HEAD with GET status and headers but no body", async () => { |
| 389 | const fetchMock = vi.fn(async (_url: string) => |
| 390 | new Response(desktopManifestText("v1.2.0-preview.7"), { status: 200 }), |
| 391 | ); |
| 392 | vi.stubGlobal("fetch", fetchMock); |
| 393 | |
| 394 | const response = await worker.fetch( |
| 395 | new Request("https://crash.reasonix.io/v1/desktop/releases/preview/latest.json", { |
| 396 | method: "HEAD", |
| 397 | }), |
| 398 | env, |
| 399 | ); |
| 400 | |
| 401 | expect(response.status).toBe(200); |
| 402 | expect(await response.text()).toBe(""); |
| 403 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 404 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-preview"); |
| 405 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 406 | }); |
| 407 | |
| 408 | it("returns a CORS-aware 405 for unsupported release methods", async () => { |
| 409 | const fetchMock = vi.fn(); |
| 410 | vi.stubGlobal("fetch", fetchMock); |
| 411 | |
| 412 | const response = await worker.fetch( |
| 413 | new Request("https://crash.reasonix.io/v1/cli/releases/preview/latest.json", { |
| 414 | method: "POST", |
| 415 | }), |
| 416 | env, |
| 417 | ); |
| 418 | |
| 419 | expect(response.status).toBe(405); |
| 420 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 421 | expect(response.headers.get("allow")).toBe("GET, HEAD, OPTIONS"); |
| 422 | expect(fetchMock).not.toHaveBeenCalled(); |
| 423 | }); |
| 424 | }); |
| 425 | |
| 426 | describe("CLI public release gateway", () => { |
| 427 | it("recognizes only Stable and Preview routes", () => { |
| 428 | expect(cliReleaseChannel("/v1/cli/releases/stable/latest.json")).toBe("stable"); |
| 429 | expect(cliReleaseChannel("/v1/cli/releases/preview/latest.json")).toBe("preview"); |
| 430 | expect(cliReleaseChannel("/v1/cli/releases/rc/latest.json")).toBeNull(); |
| 431 | }); |
| 432 | |
| 433 | it("serves a complete strict Preview pointer from R2", async () => { |
| 434 | const fetchMock = vi.fn(async (_url: string) => |
| 435 | new Response(JSON.stringify(cliRelease("v1.18.0-preview.1", true)), { status: 200 }), |
| 436 | ); |
| 437 | vi.stubGlobal("fetch", fetchMock); |
| 438 | |
| 439 | const response = await handleCLIRelease("preview"); |
| 440 | const body = await response.json() as { tag_name?: string }; |
| 441 | |
| 442 | expect(body.tag_name).toBe("v1.18.0-preview.1"); |
| 443 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 444 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-cli-preview"); |
| 445 | expect(fetchMock.mock.calls[0]?.[0]).toBe("https://dl.reasonix.io/cli/preview/latest.json"); |
| 446 | }); |
| 447 | |
| 448 | it("rewrites release notes to the canonical repository tag URL", async () => { |
| 449 | const release = cliRelease("v1.18.0", false); |
| 450 | release.html_url = "https://evil.invalid/phishing"; |
| 451 | const fetchMock = vi.fn(async () => |
| 452 | new Response(JSON.stringify(release), { status: 200 }), |
| 453 | ); |
| 454 | vi.stubGlobal("fetch", fetchMock); |
| 455 | |
| 456 | const response = await handleCLIRelease("stable"); |
| 457 | const body = await response.json() as { html_url?: string }; |
| 458 | |
| 459 | expect(body.html_url).toBe( |
| 460 | "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.18.0", |
| 461 | ); |
| 462 | }); |
| 463 | |
| 464 | it("falls back after an invalid 200 and strictly filters GitHub releases", async () => { |
| 465 | const invalidPointer = cliRelease("v1.18.0-preview.20", true); |
| 466 | invalidPointer.assets[0]!.size = 0; |
| 467 | const releases = [ |
| 468 | cliRelease("v1.19.0-rc.1", true), |
| 469 | cliRelease("v1.18.0-preview.2", true), |
| 470 | cliRelease("v1.18.0-preview.12", true), |
| 471 | cliRelease("v1.18.0-preview.13", false), |
| 472 | cliRelease("v1.17.21", false), |
| 473 | ]; |
| 474 | const fetchMock = vi |
| 475 | .fn() |
| 476 | .mockResolvedValueOnce(new Response(JSON.stringify(invalidPointer), { status: 200 })) |
| 477 | .mockResolvedValueOnce(new Response(JSON.stringify(releases), { status: 200 })); |
| 478 | vi.stubGlobal("fetch", fetchMock); |
| 479 | |
| 480 | const response = await handleCLIRelease("preview"); |
| 481 | const body = await response.json() as { tag_name?: string }; |
| 482 | |
| 483 | expect(body.tag_name).toBe("v1.18.0-preview.12"); |
| 484 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 485 | expect(fetchMock.mock.calls[1]?.[0]).toContain("releases?per_page=100"); |
| 486 | }); |
| 487 | |
| 488 | it("requires every CLI asset URL to be canonical", async () => { |
| 489 | const invalidURLs = [ |
| 490 | "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz", |
| 491 | "https://github.com@evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz", |
| 492 | "http://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz", |
| 493 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.0/reasonix-darwin-amd64.tar.gz", |
| 494 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-arm64.tar.gz", |
| 495 | ]; |
| 496 | |
| 497 | for (const browserDownloadURL of invalidURLs) { |
| 498 | const invalid = cliRelease("v1.20.0", false); |
| 499 | invalid.assets[0]!.browser_download_url = browserDownloadURL; |
| 500 | const fetchMock = vi |
| 501 | .fn() |
| 502 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 503 | .mockResolvedValueOnce(new Response(JSON.stringify([cliRelease("v1.19.0", false)]), { status: 200 })); |
| 504 | vi.stubGlobal("fetch", fetchMock); |
| 505 | |
| 506 | const response = await handleCLIRelease("stable"); |
| 507 | const body = await response.json() as { tag_name?: string }; |
| 508 | |
| 509 | expect(body.tag_name).toBe("v1.19.0"); |
| 510 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 511 | vi.unstubAllGlobals(); |
| 512 | } |
| 513 | }); |
| 514 | |
| 515 | it("rejects non-positive, malformed, and oversized CLI asset sizes", async () => { |
| 516 | const invalidSizes: unknown[] = [ |
| 517 | 0, |
| 518 | -1, |
| 519 | "42", |
| 520 | undefined, |
| 521 | 1.5, |
| 522 | 1073741825, |
| 523 | Number.MAX_SAFE_INTEGER + 1, |
| 524 | NaN, |
| 525 | ]; |
| 526 | for (const size of invalidSizes) { |
| 527 | const invalid = cliRelease("v1.20.0", false); |
| 528 | (invalid.assets[0] as { size?: unknown }).size = size; |
| 529 | const fetchMock = vi |
| 530 | .fn() |
| 531 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 532 | .mockResolvedValueOnce(new Response(JSON.stringify([cliRelease("v1.19.0", false)]), { status: 200 })); |
| 533 | vi.stubGlobal("fetch", fetchMock); |
| 534 | |
| 535 | const response = await handleCLIRelease("stable"); |
| 536 | const body = await response.json() as { tag_name?: string }; |
| 537 | |
| 538 | expect(body.tag_name, `size ${String(size)}`).toBe("v1.19.0"); |
| 539 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 540 | vi.unstubAllGlobals(); |
| 541 | } |
| 542 | }); |
| 543 | |
| 544 | it("rejects duplicate required CLI assets", async () => { |
| 545 | const invalid = cliRelease("v1.20.0", false); |
| 546 | invalid.assets.push({ ...invalid.assets[0]! }); |
| 547 | const fetchMock = vi |
| 548 | .fn() |
| 549 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 550 | .mockResolvedValueOnce(new Response(JSON.stringify([cliRelease("v1.19.0", false)]), { status: 200 })); |
| 551 | vi.stubGlobal("fetch", fetchMock); |
| 552 | |
| 553 | const response = await handleCLIRelease("stable"); |
| 554 | const body = await response.json() as { tag_name?: string }; |
| 555 | |
| 556 | expect(body.tag_name).toBe("v1.19.0"); |
| 557 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 558 | }); |
| 559 | |
| 560 | it("rejects incomplete releases and compares huge Stable versions exactly", async () => { |
| 561 | const incomplete = cliRelease("v100000000000000000001.0.0", false); |
| 562 | incomplete.assets.pop(); |
| 563 | const releases = [ |
| 564 | incomplete, |
| 565 | cliRelease("v99999999999999999999.999.999", false), |
| 566 | cliRelease("v100000000000000000000.0.0", false), |
| 567 | ]; |
| 568 | const fetchMock = vi |
| 569 | .fn() |
| 570 | .mockResolvedValueOnce(new Response(JSON.stringify(incomplete), { status: 200 })) |
| 571 | .mockResolvedValueOnce(new Response(JSON.stringify(releases), { status: 200 })); |
| 572 | vi.stubGlobal("fetch", fetchMock); |
| 573 | |
| 574 | const response = await handleCLIRelease("stable"); |
| 575 | const body = await response.json() as { tag_name?: string }; |
| 576 | |
| 577 | expect(body.tag_name).toBe("v100000000000000000000.0.0"); |
| 578 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 579 | expect(fetchMock).toHaveBeenCalledTimes(2); |
| 580 | }); |
| 581 | }); |
| 582 |