ddf0ddf71e
Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1. Build: - версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и замораживается в metadata пакета (version + immutable url + sha256); - compatibility gate: реальный бинарник должен принять канонический конфиг HY2XS для gecko и salamander до создания пакета; - сборка прогоняет тесты оркестратора и админки. Конфигурационный контракт: - HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast; - obfs стал настоящим union gecko|salamander, gecko — default; - obfs-блок рендерится оркестратором целиком, два подтипа одновременно структурно невозможны; - современный baseline: congestion bbr/standard, disableLossCompensation=false, disableStatelessReset=false, полный quic-блок. Исправления: - share URI для gecko: генератор был завязан на Obfs.Salamander.Password и выдавал нерабочую ссылку при любой другой обфускации; - SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file; - экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль; - экспорт терял неизвестные upstream-поля при round-trip через типизированную модель; - renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом; - namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0). Тесты: - 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover; - тесты URI и экспорта в Go; - tools/test/e2e-hysteria.sh с реальным клиентом Hysteria. UX: - подсказки и примеры в форме создания пира. Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов), документация на русском.
221 lines
9.0 KiB
TypeScript
221 lines
9.0 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
HYSTERIA_LINUX_AMD64_ASSET,
|
|
compareVersionParts,
|
|
parseAppTag,
|
|
parseVersion,
|
|
renderLockEnv,
|
|
selectLatestStableRelease,
|
|
selectLinuxAmd64Asset,
|
|
selectReleaseByVersion,
|
|
type GithubRelease
|
|
} from "../src/build/hysteriaRelease";
|
|
|
|
/** Upstream отдаёт URL с неэкранированным слэшем в теге; берём его как есть. */
|
|
function assetUrl(tag: string): string {
|
|
return `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${HYSTERIA_LINUX_AMD64_ASSET}`;
|
|
}
|
|
|
|
function release(tag: string, overrides: Partial<GithubRelease> = {}): 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) }],
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
/** Реалистичная выборка: stable, prerelease, draft, чужие семейства тегов. */
|
|
const UPSTREAM_FIXTURE: GithubRelease[] = [
|
|
release("app/v2.13.0-rc1", { prerelease: true }),
|
|
release("app/v2.13.0", { prerelease: true }),
|
|
release("app/v2.14.0", { draft: true }),
|
|
release("app/v2.12.2"),
|
|
release("app/v2.12.1"),
|
|
release("app/v2.9.10"),
|
|
release("app/v2.9.2"),
|
|
release("core/v2.12.2"),
|
|
release("docs/v1.0.0"),
|
|
release("v2.12.2")
|
|
];
|
|
|
|
describe("selectLatestStableRelease", () => {
|
|
test("выбирает последнюю стабильную app-версию", () => {
|
|
const resolved = selectLatestStableRelease(UPSTREAM_FIXTURE);
|
|
expect(resolved.version).toBe("v2.12.2");
|
|
expect(resolved.tag).toBe("app/v2.12.2");
|
|
});
|
|
|
|
test("игнорирует prerelease", () => {
|
|
const resolved = selectLatestStableRelease([release("app/v2.12.2"), release("app/v2.13.0", { prerelease: true })]);
|
|
expect(resolved.version).toBe("v2.12.2");
|
|
});
|
|
|
|
test("игнорирует draft", () => {
|
|
const resolved = selectLatestStableRelease([release("app/v2.12.2"), release("app/v2.13.0", { draft: true })]);
|
|
expect(resolved.version).toBe("v2.12.2");
|
|
});
|
|
|
|
test("игнорирует чужие семейства тегов", () => {
|
|
const resolved = selectLatestStableRelease([release("core/v9.9.9"), release("app/v2.12.2")]);
|
|
expect(resolved.version).toBe("v2.12.2");
|
|
});
|
|
|
|
test("игнорирует тег без префикса app/", () => {
|
|
expect(() => selectLatestStableRelease([release("v2.12.2")])).toThrow(/no stable/);
|
|
});
|
|
|
|
test("сравнение числовое, а не лексикографическое", () => {
|
|
const resolved = selectLatestStableRelease([release("app/v2.9.2"), release("app/v2.9.10")]);
|
|
expect(resolved.version).toBe("v2.9.10");
|
|
});
|
|
|
|
test("порядок элементов в ответе API не влияет на результат", () => {
|
|
const reversed = [...UPSTREAM_FIXTURE].reverse();
|
|
expect(selectLatestStableRelease(reversed).version).toBe("v2.12.2");
|
|
});
|
|
|
|
test("невалидный semver в теге отбрасывается", () => {
|
|
expect(() => selectLatestStableRelease([release("app/v2.12"), release("app/vNEXT")])).toThrow(
|
|
/no stable/
|
|
);
|
|
});
|
|
|
|
test("пустой список релизов даёт понятную ошибку", () => {
|
|
expect(() => selectLatestStableRelease([])).toThrow(/no stable/);
|
|
});
|
|
|
|
test("берёт browser_download_url ровно как отдал API, без пересборки строки", () => {
|
|
const resolved = selectLatestStableRelease(UPSTREAM_FIXTURE);
|
|
expect(resolved.artifactUrl).toBe(
|
|
"https://github.com/HyNetworks/hysteria/releases/download/app/v2.12.2/hysteria-linux-amd64"
|
|
);
|
|
});
|
|
|
|
test("процентное кодирование в url upstream не переписывается", () => {
|
|
const encoded = "https://github.com/HyNetworks/hysteria/releases/download/app%2Fv2.12.2/hysteria-linux-amd64";
|
|
const resolved = selectLatestStableRelease([
|
|
release("app/v2.12.2", {
|
|
assets: [{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: encoded }]
|
|
})
|
|
]);
|
|
expect(resolved.artifactUrl).toBe(encoded);
|
|
});
|
|
});
|
|
|
|
describe("selectLinuxAmd64Asset", () => {
|
|
test("отсутствующий linux-amd64 asset — ошибка", () => {
|
|
const broken = release("app/v2.12.2", {
|
|
assets: [{ name: "hysteria-linux-arm64", browser_download_url: assetUrl("app/v2.12.2") }]
|
|
});
|
|
expect(() => selectLatestStableRelease([broken])).toThrow(/has no hysteria-linux-amd64 asset/);
|
|
});
|
|
|
|
test("дублирующийся asset — ошибка, а не случайный выбор", () => {
|
|
const ambiguous = release("app/v2.12.2", {
|
|
assets: [
|
|
{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: assetUrl("app/v2.12.2") },
|
|
{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: "https://example.com/evil" }
|
|
]
|
|
});
|
|
expect(() => selectLatestStableRelease([ambiguous])).toThrow(/ambiguous/);
|
|
});
|
|
|
|
test("non-https url отклоняется", () => {
|
|
const insecure = release("app/v2.12.2", {
|
|
assets: [{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: "http://github.com/x" }]
|
|
});
|
|
expect(() => selectLinuxAmd64Asset(insecure)).toThrow(/not https/);
|
|
});
|
|
|
|
test("пустой url отклоняется", () => {
|
|
const empty = release("app/v2.12.2", {
|
|
assets: [{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: "" }]
|
|
});
|
|
expect(() => selectLinuxAmd64Asset(empty)).toThrow(/empty asset download url/);
|
|
});
|
|
|
|
test("частичное совпадение имени asset не принимается", () => {
|
|
const partial = release("app/v2.12.2", {
|
|
assets: [
|
|
{ name: "hysteria-linux-amd64-avx", browser_download_url: assetUrl("app/v2.12.2") }
|
|
]
|
|
});
|
|
expect(() => selectLinuxAmd64Asset(partial)).toThrow(/has no hysteria-linux-amd64 asset/);
|
|
});
|
|
});
|
|
|
|
describe("selectReleaseByVersion", () => {
|
|
test("находит явно запрошенную версию, даже если она не последняя", () => {
|
|
const resolved = selectReleaseByVersion(UPSTREAM_FIXTURE, "v2.12.1");
|
|
expect(resolved.version).toBe("v2.12.1");
|
|
expect(resolved.tag).toBe("app/v2.12.1");
|
|
});
|
|
|
|
test("позволяет осознанно закрепить prerelease по явному запросу", () => {
|
|
const resolved = selectReleaseByVersion(UPSTREAM_FIXTURE, "v2.13.0");
|
|
expect(resolved.tag).toBe("app/v2.13.0");
|
|
// при этом автоматический выбор её по-прежнему не берёт
|
|
expect(selectLatestStableRelease(UPSTREAM_FIXTURE).version).toBe("v2.12.2");
|
|
});
|
|
|
|
test("draft по явному запросу отклоняется", () => {
|
|
expect(() => selectReleaseByVersion(UPSTREAM_FIXTURE, "v2.14.0")).toThrow(/draft release/);
|
|
});
|
|
|
|
test("несуществующая версия даёт ошибку", () => {
|
|
expect(() => selectReleaseByVersion(UPSTREAM_FIXTURE, "v9.9.9")).toThrow(/was not found/);
|
|
});
|
|
|
|
test("невалидный формат override отклоняется", () => {
|
|
for (const bad of ["2.12.2", "v2.12", "latest", ""]) {
|
|
expect(() => selectReleaseByVersion(UPSTREAM_FIXTURE, bad)).toThrow(/invalid Hysteria version override/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("парсинг версий", () => {
|
|
test("parseAppTag", () => {
|
|
expect(parseAppTag("app/v2.12.2")?.version).toBe("v2.12.2");
|
|
expect(parseAppTag(" app/v2.12.2 ")?.version).toBe("v2.12.2");
|
|
expect(parseAppTag("app/v2.12.2-rc1")).toBeNull();
|
|
expect(parseAppTag("app/2.12.2")).toBeNull();
|
|
});
|
|
|
|
test("parseVersion", () => {
|
|
expect(parseVersion("v2.12.2")).toEqual([2, 12, 2]);
|
|
expect(parseVersion("v2.12.2-rc1")).toBeNull();
|
|
});
|
|
|
|
test("compareVersionParts", () => {
|
|
expect(compareVersionParts([2, 12, 2], [2, 12, 1])).toBe(1);
|
|
expect(compareVersionParts([2, 9, 10], [2, 9, 2])).toBe(1);
|
|
expect(compareVersionParts([2, 12, 2], [2, 12, 2])).toBe(0);
|
|
expect(compareVersionParts([1, 99, 99], [2, 0, 0])).toBe(-1);
|
|
});
|
|
});
|
|
|
|
describe("renderLockEnv", () => {
|
|
const resolved = selectLatestStableRelease(UPSTREAM_FIXTURE);
|
|
const sha = "a".repeat(64);
|
|
|
|
test("содержит замороженные версию, url и sha256", () => {
|
|
const rendered = renderLockEnv(resolved, sha, "latest-stable", "2026-08-27T00:00:00Z");
|
|
expect(rendered).toContain("HYSTERIA_VERSION=v2.12.2");
|
|
expect(rendered).toContain(`HYSTERIA_ARTIFACT_URL=${resolved.artifactUrl}`);
|
|
expect(rendered).toContain(`HYSTERIA_ARTIFACT_SHA256=${sha}`);
|
|
expect(rendered).toContain("HYSTERIA_RESOLUTION=latest-stable");
|
|
expect(rendered).toContain("HYSTERIA_RESOLVED_AT=2026-08-27T00:00:00Z");
|
|
});
|
|
|
|
test("невалидный sha256 отклоняется", () => {
|
|
for (const bad of ["", "abc", "replace-with-real-sha256", "A".repeat(64), "z".repeat(64)]) {
|
|
expect(() => renderLockEnv(resolved, bad, "latest-stable", "2026-08-27T00:00:00Z")).toThrow(
|
|
/invalid Hysteria artifact sha256/
|
|
);
|
|
}
|
|
});
|
|
});
|