import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { HYSTERIA_HASHES_ASSET, HYSTERIA_LINUX_AMD64_ASSET, selectLatestStableRelease, type GithubRelease, } from "../src/build/hysteriaRelease"; function assetUrl(tag: string, name: string): string { return `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${name}`; } function release(tag: string): GithubRelease { return { tag_name: tag, draft: false, prerelease: false, published_at: "2026-08-23T00:59:00Z", assets: [ { name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: assetUrl(tag, HYSTERIA_LINUX_AMD64_ASSET), }, { name: HYSTERIA_HASHES_ASSET, browser_download_url: assetUrl(tag, HYSTERIA_HASHES_ASSET), }, ], }; } function srcFile(...parts: string[]): string { return readFileSync(join(import.meta.dir, "..", "src", ...parts), "utf8"); } describe("release rollover", () => { // Пакет A собран сегодня, пакет B — завтра. Выход новой upstream-версии // не должен менять содержимое уже собранного пакета A. const upstreamToday = [release("app/v2.12.1"), release("app/v2.12.2")]; const upstreamTomorrow = [...upstreamToday, release("app/v2.12.3")]; test("сборка сегодня закрепляет 2.12.2", () => { expect(selectLatestStableRelease(upstreamToday).version).toBe("v2.12.2"); }); test("сборка завтра закрепляет 2.12.3", () => { expect(selectLatestStableRelease(upstreamTomorrow).version).toBe("v2.12.3"); }); test("резолв чистый: тот же список всегда даёт тот же результат", () => { const first = selectLatestStableRelease(upstreamToday); const second = selectLatestStableRelease(upstreamToday); expect(first).toEqual(second); }); }); describe("install-time никогда не резолвит latest", () => { // Гарантия acceptance-критерия: "latest" относится к моменту сборки, // поэтому переустановка старого пакета обязана ставить старую версию. const installTimeSources = [ ["commands", "install.ts"], ["steps", "hysteria.ts"], ["lib", "packageMeta.ts"], ["commands", "reconfigure.ts"], ]; test("install-time код не импортирует build-time резолвер", () => { for (const parts of installTimeSources) { expect(srcFile(...parts)).not.toContain("build/hysteriaRelease"); } }); test("install-time код не обращается к upstream API и не использует moving latest", () => { for (const parts of installTimeSources) { const source = srcFile(...parts); expect(source).not.toContain("api.github.com"); expect(source).not.toContain("download.hysteria.network"); expect(source).not.toMatch(/releases\/latest/); } }); test("установка берёт версию, url и sha256 только из metadata пакета", () => { const install = srcFile("commands", "install.ts"); for (const key of ["hysteria.version", "hysteria.url", "hysteria.sha256"]) { expect(install).toMatch( new RegExp( `readPackageValue\\(\\s*options\\.packageDir,\\s*"${key.replace(".", "\\.")}"`, ), ); } }); test("отсутствие любой части lock-метаданных прерывает установку", () => { expect(srcFile("commands", "install.ts")).toContain( "missing Hysteria lock metadata in package", ); }); test("установленный бинарник сверяется с закреплённой версией", () => { const step = srcFile("steps", "hysteria.ts"); expect(step).toContain("installed Hysteria version mismatch"); expect(step).toContain("sha256sum -c -"); }); });