const { execFile } = require("node:child_process"); const { mkdtemp, readFile, rm, writeFile } = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); const { promisify } = require("node:util"); const execFileAsync = promisify(execFile); const ROOT_DIR = path.resolve(__dirname, "../../../.."); const VALIDATOR = path.join(ROOT_DIR, "deploy/trialrun/validate-env-contract.js"); const CLASSIFICATION = path.join( ROOT_DIR, "deploy/trialrun/env-recovery-classification.json", ); describe("environment recovery classification contract", () => { let fixtureRoot; let baseClassification; beforeAll(async () => { baseClassification = JSON.parse(await readFile(CLASSIFICATION, "utf8")); }); beforeEach(async () => { fixtureRoot = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-env-recovery-")); }); afterEach(async () => { await rm(fixtureRoot, { recursive: true, force: true }); }); async function validate(classification = baseClassification) { const fixturePath = path.join(fixtureRoot, "classification.json"); await writeFile(fixturePath, `${JSON.stringify(classification, null, 2)}\n`); return execFileAsync("node", [VALIDATOR], { cwd: ROOT_DIR, env: { ...process.env, ENV_CONTRACT_RECOVERY_CLASSIFICATION_PATH: fixturePath, }, }); } it("classifies every env contract key exactly once", async () => { const result = await validate(); expect(result.stdout).toContain("环境恢复分类键数: 166"); }); it("skips tracked paths deleted in the current worktree before reading metadata", async () => { const source = await readFile(VALIDATOR, "utf8"); const trackedFileLoop = source.indexOf("for (const relativePath of trackedFiles)"); const existsGuard = source.indexOf("if (!fs.existsSync(filePath)) continue;", trackedFileLoop); const statRead = source.indexOf("fs.statSync(filePath)", trackedFileLoop); expect(trackedFileLoop).toBeGreaterThanOrEqual(0); expect(existsGuard).toBeGreaterThan(trackedFileLoop); expect(statRead).toBeGreaterThan(existsGuard); }); it("rejects a missing key", async () => { const classification = structuredClone(baseClassification); classification.categories.template_rebuild = classification.categories.template_rebuild .filter((key) => key !== "APP_TIMEZONE"); await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("环境恢复分类缺少 APP_TIMEZONE"), }); }); it("rejects duplicate classification", async () => { const classification = structuredClone(baseClassification); classification.categories.direct_rotate.push("APP_TIMEZONE"); await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("环境恢复键 APP_TIMEZONE 重复分类"), }); }); it("rejects keys outside the env contract", async () => { const classification = structuredClone(baseClassification); classification.categories.template_rebuild.push("SYNTHETIC_EXTRA_KEY"); await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("环境恢复分类包含 .env.example 外键 SYNTHETIC_EXTRA_KEY"), }); }); it("rejects unknown categories", async () => { const classification = structuredClone(baseClassification); classification.categories.unreviewed = []; await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("环境恢复分类清单存在未知类别 unreviewed"), }); }); it("rejects illegal key names and suspected values", async () => { const classification = structuredClone(baseClassification); classification.categories.template_rebuild.push("JWT_SECRET=not-a-key-name"); await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("含非法键名或疑似值"), }); }); it("keeps FIELD_ENCRYPTION_KEY in preserve_original", async () => { const classification = structuredClone(baseClassification); classification.categories.preserve_original = []; classification.categories.direct_rotate.push("FIELD_ENCRYPTION_KEY"); await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("FIELD_ENCRYPTION_KEY 必须归入 preserve_original"), }); }); it("keeps POSTGRES_PASSWORD in downtime_reset", async () => { const classification = structuredClone(baseClassification); classification.categories.downtime_reset = []; classification.categories.direct_rotate.push("POSTGRES_PASSWORD"); await expect(validate(classification)).rejects.toMatchObject({ stderr: expect.stringContaining("POSTGRES_PASSWORD 必须归入 downtime_reset"), }); }); });