文件
hyb-oa/server/tests/unit/scripts/deployment-safety-contract.test.js
T
wangsli 37b4ef2d10
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
恢复付款入口与常开基线(08-01-reimbursement-notification-progress-clarity)
2026-08-01 23:12:23 +08:00

127 行
6.8 KiB
JavaScript

const { execFile } = require("node:child_process");
const { mkdtemp, mkdir, rm, symlink, 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 PREFLIGHT = path.join(ROOT_DIR, "deploy/trialrun/preflight-remote.sh");
describe("deployment safety contract", () => {
let fixtureRoot;
let remoteDir;
let envFile;
let postgresDir;
let storageDir;
beforeEach(async () => {
fixtureRoot = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-deploy-safety-"));
remoteDir = path.join(fixtureRoot, "app");
envFile = path.join(remoteDir, "deploy/trialrun/.env");
postgresDir = path.join(fixtureRoot, "postgres");
storageDir = path.join(fixtureRoot, "storage");
await mkdir(path.join(remoteDir, "server"), { recursive: true });
await mkdir(path.join(remoteDir, "deploy/trialrun"), { recursive: true });
await mkdir(postgresDir, { recursive: true });
await mkdir(storageDir, { recursive: true });
await writeFile(path.join(remoteDir, "server/package.json"), "{}\n");
await writeFile(path.join(remoteDir, "deploy/trialrun/docker-compose.yml"), "services: {}\n");
await writeFile(envFile, "APP_ORIGIN=https://example.invalid\n", { mode: 0o600 });
await writeFile(path.join(postgresDir, "PG_VERSION"), "16\n");
});
afterEach(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
function preflight(overrides = {}) {
return execFileAsync("bash", [PREFLIGHT], {
env: {
...process.env,
REMOTE_DIR: remoteDir,
REMOTE_ENV_FILE: envFile,
COMPOSE_FILE: "deploy/trialrun/docker-compose.yml",
POSTGRES_DATA_DIR: postgresDir,
STORAGE_HOST_DIR: storageDir,
DEPLOY_DATA_MODE: "existing",
SYNC_ENV: "0",
...overrides,
},
});
}
it("accepts a complete existing deployment contract", async () => {
await expect(preflight()).resolves.toMatchObject({ stdout: expect.stringContaining("[remote-preflight][PASS]") });
});
it("rejects a config directory masquerading as the repository root", async () => {
await expect(preflight({ REMOTE_DIR: path.join(remoteDir, "deploy/trialrun") })).rejects.toMatchObject({ code: 1 });
});
it("rejects a missing or symbolic remote env before synchronization", async () => {
await rm(envFile);
await expect(preflight()).rejects.toMatchObject({ code: 1 });
await symlink(path.join(remoteDir, "missing.env"), envFile);
await expect(preflight()).rejects.toMatchObject({ code: 1 });
});
it("rejects invalid existing data directories", async () => {
await rm(path.join(postgresDir, "PG_VERSION"));
await expect(preflight()).rejects.toMatchObject({ code: 1 });
});
it("runs remote preflight before rsync and reads existing contracts from systemd", async () => {
const deploy = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "deploy/trialrun/deploy.sh"), "utf8");
const wrapper = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "deploy/trialrun/deploy-existing.sh"), "utf8");
expect(deploy.indexOf("REMOTE_PREFLIGHT_SCRIPT")).toBeGreaterThanOrEqual(0);
expect(deploy.indexOf('>>> 3. 同步前只读校验远端部署合同')).toBeLessThan(deploy.indexOf("rsync -az --delete"));
expect(deploy.indexOf('>>> 3. 同步前只读校验远端部署合同')).toBeLessThan(deploy.indexOf("mkdir -p '${REMOTE_DIR}'"));
expect(wrapper).toContain("systemctl show '${DEPLOY_CONTRACT_UNIT}' -p Environment --value");
expect(wrapper).not.toContain("docker inspect");
expect(wrapper).not.toContain("working_dir");
expect(wrapper).toContain("DEPLOY_DATA_MODE=existing");
expect(wrapper).toContain("SYNC_ENV=0");
});
it("keeps immutable build args after stable dependency layers", async () => {
const dockerfile = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "deploy/trialrun/Dockerfile"), "utf8");
expect(dockerfile.indexOf("RUN apk add --no-cache bash curl")).toBeLessThan(dockerfile.indexOf("ARG VITE_BUILD_SHA"));
expect(dockerfile.indexOf("RUN cd client && npm ci")).toBeLessThan(dockerfile.indexOf("ARG VITE_BUILD_SHA"));
expect(dockerfile.indexOf("RUN cd server && npm ci")).toBeLessThan(dockerfile.indexOf("ARG BUILD_SHA"));
});
it("keeps deployment smoke output free of raw account and external identifiers", async () => {
const seed = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "server/src/scripts/seed-trialrun.js"), "utf8");
const external = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "server/src/scripts/smoke-external-deps.js"), "utf8");
const runtime = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "server/src/scripts/smoke-trialrun-runtime.js"), "utf8");
expect(seed).not.toContain("${adminEmail}");
expect(seed).not.toContain("${adminPhone}");
expect(seed).not.toContain("${result.password}");
expect(seed).not.toContain("generateOneTimePassword");
expect(external).not.toContain("corpId=${config.wecomCorpId}");
expect(external).not.toContain("redirectUri=${config.wecomRedirectUri}");
expect(external).not.toContain("from=${config.emailFrom}");
expect(runtime).not.toContain("${adminEmployee.employee_no}");
});
it("keeps the formal payment batch baseline enabled and restores QA caller state", async () => {
const envExample = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "deploy/trialrun/.env.example"), "utf8");
const runtime = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "server/src/scripts/smoke-trialrun-runtime.js"), "utf8");
const shortWindow = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "scripts/qa/run-p2-fin-live-window.sh"), "utf8");
const enableWindow = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "scripts/qa/run-fin-prod-enable-window.sh"), "utf8");
const liveSpec = await require("node:fs/promises").readFile(path.join(ROOT_DIR, "client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts"), "utf8");
expect(envExample).toMatch(/^PAYMENT_BATCH_ENABLED=true$/m);
expect(runtime).toContain("PAYMENT_BATCH_ENABLED: 'true'");
expect(runtime).toContain("FEATURE_FLAG_KEYS.paymentBatch");
expect(shortWindow).toContain("restore-caller-flag-snapshot");
expect(shortWindow).toContain("P2_FIN_LIVE_ALREADY_ON='$ALREADY_ON_MODE'");
expect(enableWindow).toContain("restore-caller-flag-snapshot");
expect(liveSpec).toContain("restoreFlagSnapshot");
expect(liveSpec).not.toContain("forcePaymentBatchOff");
expect(shortWindow).not.toContain("force_payment_batch_flag_off");
expect(enableWindow).not.toContain("force_payment_batch_flag_off");
});
});