118 行
4.7 KiB
JavaScript
118 行
4.7 KiB
JavaScript
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("环境恢复分类键数: 165");
|
|
});
|
|
|
|
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"),
|
|
});
|
|
});
|
|
});
|