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"); const AGGREGATE_PREFLIGHT = path.join(ROOT_DIR, "deploy/trialrun/check-existing-deployment.sh"); const REFRESH_HELPER = path.join(ROOT_DIR, "scripts/refresh-deployment-contract.sh"); const REFRESH_INSTALLER = path.join(ROOT_DIR, "deploy/trialrun/install-deployment-contract-refresh.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("check-existing-deployment.sh"); expect(wrapper.indexOf("check-existing-deployment.sh")).toBeLessThan(wrapper.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"')); 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"); expect(wrapper).toContain("DEPLOY_DEFER_FINAL_SUCCESS=1"); expect(wrapper.indexOf('ssh "$DEPLOY_REFRESH_SERVER" refresh')).toBeGreaterThan(wrapper.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"')); expect(wrapper).toContain("DEPLOYED_BUT_CONTRACT_REFRESH_FAILED"); }); it("aggregates preflight checks and keeps the root helper self-contained", async () => { const aggregate = await require("node:fs/promises").readFile(AGGREGATE_PREFLIGHT, "utf8"); const helper = await require("node:fs/promises").readFile(REFRESH_HELPER, "utf8"); const installer = await require("node:fs/promises").readFile(REFRESH_INSTALLER, "utf8"); expect(aggregate).toContain("set -uo pipefail"); expect(aggregate).not.toMatch(/^set\s+-[A-Za-z]*e[A-Za-z]*(?:\s|$)/m); expect(aggregate).toContain("CHECK_NAMES=("); expect(aggregate).toContain("root_ssh_identity"); expect(aggregate).toContain("refresh_program"); expect(helper).toContain("SSH_ORIGINAL_COMMAND"); expect(helper).toContain("20-active-build.conf"); expect(helper).toContain("refresh_program_integrity_invalid"); expect(helper).not.toContain("eval "); expect(helper).not.toContain("bash -c"); expect(helper).not.toContain("REMOTE_DIR/"); expect(installer).toContain("dedicated key already has unmanaged or unrestricted root authorization"); expect(installer).toContain("no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,no-user-rc"); expect(installer).toContain("managed forced-command marker is malformed"); }); 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"); }); });