import { describe, expect, test } from "bun:test"; import { HY2XS_PRODUCT_ID, assertCurrentGeneration, buildInstallStateRecord, detectGenerationProblems, renderGenerationFailure } from "../src/lib/installState"; import { HY2XS_CONFIG_SCHEMA_VERSION, HY2XS_RELEASE_LINE } from "../src/config/profile"; function currentState(overrides: Record = {}): Record { return { product: HY2XS_PRODUCT_ID, release_line: HY2XS_RELEASE_LINE, config_schema_version: HY2XS_CONFIG_SCHEMA_VERSION, product_version: "1.0.0", installed: true, phase: "installed", ...overrides }; } describe("идентификация поколения установки", () => { test("маркер текущего поколения принимается", () => { expect(detectGenerationProblems(currentState())).toEqual([]); expect(() => assertCurrentGeneration(currentState())).not.toThrow(); }); // Именно этот случай и есть «старый state marker от 0.x»: installed: true // сам по себе ничего не доказывает. test("маркер без полей поколения отклоняется, несмотря на installed: true", () => { const legacy = { installed: true, phase: "installed", version: "0.0.22" }; expect(detectGenerationProblems(legacy)).toEqual([ "missing_product", "missing_release_line", "missing_config_schema" ]); expect(() => assertCurrentGeneration(legacy)).toThrow(/не относится к текущему поколению/); }); test("чужой продукт отклоняется", () => { expect(detectGenerationProblems(currentState({ product: "h-ui" }))).toContain("foreign_product"); }); test("другая линия релиза отклоняется", () => { expect(detectGenerationProblems(currentState({ release_line: 0 }))).toContain("foreign_release_line"); expect(detectGenerationProblems(currentState({ release_line: 2 }))).toContain("foreign_release_line"); }); test("другая схема конфигурации отклоняется", () => { expect(detectGenerationProblems(currentState({ config_schema_version: 1 }))).toContain( "foreign_config_schema" ); }); test("нечисловые поля поколения считаются отсутствующими", () => { const problems = detectGenerationProblems( currentState({ release_line: "1", config_schema_version: "2" }) ); expect(problems).toContain("missing_release_line"); expect(problems).toContain("missing_config_schema"); }); test("не-объект отклоняется целиком", () => { expect(detectGenerationProblems(null)).toHaveLength(3); expect(detectGenerationProblems("installed")).toHaveLength(3); expect(detectGenerationProblems([1, 2])).toHaveLength(3); }); test("сообщение объясняет расхождение и направляет на чистую переустановку", () => { const state = currentState({ release_line: 0 }); const message = renderGenerationFailure(detectGenerationProblems(state), state); expect(message).toContain("release_line"); expect(message).toContain("docs/operations/14-legacy-cleanup.md"); }); }); describe("запись маркера установки", () => { test("маркер всегда несёт идентификацию поколения", () => { const record = buildInstallStateRecord({ productVersion: "1.0.0", buildId: "test-build", opId: "op", startedAt: "2026-08-27T00:00:00.000Z", phase: "installed", installed: true, ownedPaths: ["/etc/hysteria/config.yaml"] }); expect(record.product).toBe(HY2XS_PRODUCT_ID); expect(record.release_line).toBe(HY2XS_RELEASE_LINE); expect(record.config_schema_version).toBe(HY2XS_CONFIG_SCHEMA_VERSION); expect(detectGenerationProblems(record)).toEqual([]); }); test("успешная установка не несёт repair_hint", () => { const record = buildInstallStateRecord({ productVersion: "1.0.0", buildId: "b", opId: "op", startedAt: "2026-08-27T00:00:00.000Z", phase: "installed", installed: true, ownedPaths: [] }); expect(record.repair_hint).toBeUndefined(); }); test("незавершённая установка подсказывает repair с явным флагом", () => { const record = buildInstallStateRecord({ productVersion: "1.0.0", buildId: "b", opId: "op", startedAt: "2026-08-27T00:00:00.000Z", phase: "failed", installed: false, ownedPaths: [], lastError: "boom", repairHint: "run: hy2xs-orchestrator repair --allow-partial-state" }); expect(record.installed).toBe(false); expect(record.repair_hint).toContain("--allow-partial-state"); }); });