Files
ftb-project-management/scripts/ops-scripts.test.mjs
72a59f125c docs(ops): 补齐生产 runbook 和 readiness 清单
- 新增迁移回滚、AppData 退场、小宝后台任务 runbook\n- 新增生产 readiness 证据清单和 runbook placeholder 扫描\n- 更新部署文档与路线图到 V2.8 运维闭环阶段\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
2026-07-08 16:28:02 +08:00

153 lines
5.0 KiB
JavaScript

import { spawnSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
const root = resolve(import.meta.dirname, '..');
function runScript(script, args) {
return spawnSync(process.execPath, [join(root, script), ...args], {
cwd: root,
encoding: 'utf8',
});
}
describe('production ops scripts', () => {
it('prints a pg_dump command in dry-run mode without writing the target file', () => {
const dir = mkdtempSync(join(tmpdir(), 'ftb-pg-backup-'));
const output = join(dir, 'backup.dump');
const result = runScript('scripts/backup-postgres.mjs', [
'--dry-run',
'--env-file',
'.env.production.example',
'--output',
output,
]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /docker compose/);
assert.match(result.stdout, /pg_dump/);
assert.match(result.stdout, /backup\.dump/);
assert.equal(existsSync(output), false);
});
it('refuses restore by default unless --confirm-overwrite is supplied', () => {
const dir = mkdtempSync(join(tmpdir(), 'ftb-pg-restore-'));
const input = join(dir, 'backup.dump');
writeFileSync(input, 'not-a-real-dump');
const result = runScript('scripts/restore-postgres.mjs', [
'--env-file',
'.env.production.example',
'--input',
input,
]);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /--confirm-overwrite/);
});
it('prints fresh database restore steps in dry-run mode after explicit overwrite confirmation', () => {
const dir = mkdtempSync(join(tmpdir(), 'ftb-pg-restore-dry-'));
const input = join(dir, 'backup.dump');
writeFileSync(input, 'not-a-real-dump');
const result = runScript('scripts/restore-postgres.mjs', [
'--dry-run',
'--confirm-overwrite',
'--env-file',
'.env.production.example',
'--input',
input,
]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /dropdb/);
assert.match(result.stdout, /createdb/);
assert.match(result.stdout, /pg_restore/);
});
it('prints a server_data volume tar backup command in dry-run mode', () => {
const result = runScript('scripts/backup-server-data.mjs', [
'--dry-run',
'--env-file',
'.env.production.example',
]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /docker run/);
assert.match(result.stdout, /ftb_pm_server_data/);
assert.match(result.stdout, /tar -czf/);
});
it('prints read-only release smoke checks in dry-run mode', () => {
const result = runScript('scripts/smoke-test-release.mjs', [
'--dry-run',
'--base-url',
'http://localhost',
]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\/api\/v1\/health\/version/);
assert.match(result.stdout, /\/products/);
assert.match(result.stdout, /\/api\/v1\/products/);
assert.match(result.stdout, /\/api\/v1\/v2\.2\/requirements\?productId=__smoke__/);
assert.match(result.stdout, /\/api\/v1\/config\/ai/);
});
it('declares the production monitoring baseline without real alert secrets', () => {
const alertRules = readFileSync(
join(root, 'deploy/monitoring/prometheus/alert-rules.yml'),
'utf8',
);
const postgresQueries = readFileSync(
join(root, 'deploy/monitoring/postgres/postgres-queries.yml'),
'utf8',
);
const promtailConfig = readFileSync(
join(root, 'deploy/monitoring/promtail/config.yml'),
'utf8',
);
const dashboard = readFileSync(
join(root, 'deploy/monitoring/grafana/dashboards/ftb-production-overview.json'),
'utf8',
);
for (const alertName of [
'FtbPostgresDown',
'FtbDiskPressure',
'FtbSlowApiLogBurst',
'FtbSlowPrismaLogBurst',
'FtbJobFailureLogBurst',
'FtbXiaobaoSummaryStale',
]) {
assert.match(alertRules, new RegExp(alertName));
}
assert.match(postgresQueries, /xiaobao_risk_summaries/);
assert.match(postgresQueries, /dirty = true/);
assert.match(promtailConfig, /Slow API request/);
assert.match(promtailConfig, /Slow Prisma query/);
assert.match(promtailConfig, /AppData relation sync failed/);
assert.match(dashboard, /FTB Production Overview/);
const combined = `${alertRules}\n${postgresQueries}\n${promtailConfig}\n${dashboard}`;
assert.doesNotMatch(combined, /sk-ant-[A-Za-z0-9]/);
assert.doesNotMatch(combined, /hooks\.slack\.com\/services\//);
});
it('passes the runbook placeholder scan', () => {
const result = runScript('scripts/check-runbook-placeholders.mjs', [
'--paths',
'docs/runbooks',
'docs/production-readiness.md',
]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /Runbook placeholder scan passed/);
});
});