- 新增迁移回滚、AppData 退场、小宝后台任务 runbook\n- 新增生产 readiness 证据清单和 runbook placeholder 扫描\n- 更新部署文档与路线图到 V2.8 运维闭环阶段\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
153 lines
5.0 KiB
JavaScript
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/);
|
|
});
|
|
});
|