Files
baiyue-firstleg/scripts/test-env-business-reset-plan.test.mjs
T
2026-10-21 21:30:24 +08:00

169 lines
9.3 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
import { buildPlan, countSql, fingerprint, inspectionSql, readCatalog, writeReport } from './test-env-business-reset-plan.mjs'
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
const sourcePaths = ['deploy/sql/头程数据库统一增量-MySQL8.sql', 'deploy/sql/海外区域统一增量-MySQL8.sql']
const sources = sourcePaths.map(name => ({ path: name, sql: readFileSync(new URL(`../${name}`, import.meta.url), 'utf8') }))
const policy = JSON.parse(readFileSync(new URL('./test-env-business-reset-policy.json', import.meta.url), 'utf8'))
/** 构造少量离线表,覆盖新增表和保留表外键的失败关闭行为。 */
function fixture() {
const sql = `USE baiyue_order;
CREATE TABLE ord_order (
id bigint NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB COMMENT='订单';
CREATE TABLE retained_audit (
order_id bigint,
FOREIGN KEY (order_id) REFERENCES ord_order (id)
) ENGINE=InnoDB COMMENT='保留审计';
CREATE TRIGGER protect_order BEFORE DELETE ON ord_order FOR EACH ROW BEGIN END;
`
const source = { path: 'fixture.sql', sql }
return { sources: [source], policy: { version: 1, environment: 'TEST', sourceSha256: { 'fixture.sql': fingerprint(sql) }, groups: [
{ action: 'CLEAR_CANDIDATE', reason: '业务单据', tables: ['baiyue_order.ord_order'] },
{ action: 'KEEP', reason: '独立审计', tables: ['baiyue_order.retained_audit'] },
] } }
}
/** 结构清单只模拟表名及估算数量,不冒充真实环境验证。 */
function inventoryFor(plan) {
return plan.tables.map(table => ({ schema: table.schema, table: table.name, type: 'BASE TABLE', estimatedRows: 7 }))
}
test('当前每张基线表显式登记,离线结果始终不可执行', () => {
const plan = buildPlan(sources, policy)
assert.equal(plan.summary.total, readCatalog(sources).length)
assert.equal(plan.summary.total, policy.groups.flatMap(group => group.tables).length)
assert.equal(plan.executionSupported, false)
assert.equal(plan.status, 'REVIEW_REQUIRED')
assert.equal(plan.canGenerateCountQueries, false)
assert.ok(plan.blockers.some(item => item.code === 'LIVE_INVENTORY_REQUIRED'))
assert.ok(!plan.blockers.some(item => ['BASELINE_CHANGED', 'UNCLASSIFIED_TABLE', 'STALE_POLICY_TABLE'].includes(item.code)))
assert.ok(plan.tables.some(table => table.triggers.some(trigger => trigger.event === 'DELETE')))
})
test('身份、文件、报价、自营和序列保留,资金与资源混合表进入字段审阅', () => {
const tables = new Map(buildPlan(sources, policy).tables.map(table => [table.key, table]))
for (const key of ['baiyue_auth.auth_account', 'baiyue_iam.iam_tenant', 'baiyue_file.file_object',
'baiyue_file.file_pda_release', 'baiyue_audit.sys_audit_log', 'baiyue_firstleg.fl_price_book_version',
'baiyue_lastleg.ll_selfop_inventory_snapshot', 'baiyue_lastleg.ll_document_sequence', 'baiyue_order.ord_customer_address']) {
assert.equal(tables.get(key).action, 'KEEP', key)
}
for (const key of ['baiyue_finance.fin_wallet_account', 'baiyue_warehouse.wh_location',
'baiyue_lastlag_warehouse.wh_pallet', 'baiyue_lastlag_warehouse.wh_location', 'baiyue_lastleg.ll_vehicle',
'baiyue_lastleg.ll_appointment_work_guard']) assert.equal(tables.get(key).action, 'RESET_REVIEW', key)
assert.match(tables.get('baiyue_finance.fin_wallet_account').reason, /可用余额.*有效授信.*不能固定为零/)
assert.equal(tables.get('baiyue_finance.fin_recharge_request').action, 'CLEAR_CANDIDATE')
assert.equal(tables.get('baiyue_finance.fin_wallet_ledger').action, 'CLEAR_CANDIDATE')
})
test('独立审计保留而订单自身历史随单清理,人工计划与资源档案保留', () => {
const plan = buildPlan(sources, policy)
const tables = new Map(plan.tables.map(table => [table.key, table]))
for (const key of ['baiyue_order.ord_order_status_history', 'baiyue_firstleg.fl_loading_pick_history',
'baiyue_lastleg.ll_dispatch_history', 'baiyue_lastleg.ll_fleet_trip_history']) {
assert.equal(tables.get(key).action, 'CLEAR_CANDIDATE', key)
}
for (const key of ['baiyue_finance.fin_audit_log', 'baiyue_firstleg.fl_sailing',
'baiyue_firstleg.fl_sailing_history', 'baiyue_lastleg.ll_fleet_vehicle_record',
'baiyue_lastleg.ll_fleet_vehicle_record_history', 'baiyue_iam.iam_notification_task']) {
assert.equal(tables.get(key).action, 'KEEP', key)
}
assert.equal(tables.get('baiyue_file.file_export_task').action, 'REVIEW')
assert.equal(tables.get('baiyue_file.file_export_outbox').action, 'REVIEW')
assert.ok(!plan.blockers.some(item => item.code === 'RETAINED_CHILD_REFERENCES_CLEAR_PARENT'))
})
test('基线新增表不会继承相似前缀的删除建议', () => {
const data = fixture()
data.sources[0].sql += `CREATE TABLE ord_order_new (
id bigint
) ENGINE=InnoDB;
`
const plan = buildPlan(data.sources, data.policy)
assert.equal(plan.tables.find(table => table.name === 'ord_order_new').action, 'REVIEW')
assert.ok(plan.blockers.some(item => item.code === 'UNCLASSIFIED_TABLE'))
assert.ok(plan.blockers.some(item => item.code === 'BASELINE_CHANGED'))
})
test('保留表引用待清主表时明确报告,且提取删除保护触发器', () => {
const data = fixture()
const plan = buildPlan(data.sources, data.policy)
assert.ok(plan.blockers.some(item => item.code === 'RETAINED_CHILD_REFERENCES_CLEAR_PARENT'
&& item.child === 'baiyue_order.retained_audit' && item.parent === 'baiyue_order.ord_order'))
assert.deepEqual(plan.tables.find(table => table.name === 'ord_order').triggers,
[{ name: 'protect_order', timing: 'BEFORE', event: 'DELETE' }])
})
test('策略重复项、错误环境和未完整解析的 CREATE TABLE 拒绝处理', () => {
const data = fixture()
const duplicate = structuredClone(data.policy)
duplicate.groups[0].tables.push('baiyue_order.retained_audit')
assert.throws(() => buildPlan(data.sources, duplicate), /重复登记/)
assert.throws(() => buildPlan(data.sources, { ...data.policy, environment: 'PROD' }), /TEST/)
assert.throws(() => readCatalog([{ path: 'unsupported.sql', sql: 'CREATE TABLE `quoted` (id INT);' }]), /未能完整解析/)
})
test('实际清单缺表、多表或视图均阻断计数 SQL 的生成', () => {
const data = fixture()
const initial = buildPlan(data.sources, data.policy)
for (const rows of [
[],
[...inventoryFor(initial), { schema: 'baiyue_order', table: 'unclassified', type: 'BASE TABLE' }],
inventoryFor(initial).map(row => ({ ...row, type: 'VIEW' })),
]) {
const plan = buildPlan(data.sources, data.policy, rows)
assert.equal(plan.canGenerateCountQueries, false)
assert.throws(() => countSql(plan), /完整且表名吻合/)
}
})
test('完整实际表清单仅解锁 SELECT COUNT,估算行数不转为已验证精确数量', () => {
const data = fixture()
const initial = buildPlan(data.sources, data.policy)
const plan = buildPlan(data.sources, data.policy, inventoryFor(initial))
assert.equal(plan.executionSupported, false)
assert.equal(plan.status, 'REVIEW_REQUIRED')
assert.equal(plan.tables[0].estimatedRows, 7)
assert.equal(plan.tables[0].exactRows, undefined)
const sql = countSql(plan).split('\n').filter(line => line && !line.startsWith('--'))
assert.equal(sql.length, plan.tables.length)
assert.ok(sql.every(line => /^SELECT '[a-z_.]+' AS table_key, COUNT\(\*\) AS exact_rows FROM `[a-z_]+`\.`[a-z_]+`;$/.test(line)))
})
test('外部清单拒绝重复、范围外和 SQL 注入标识符', () => {
const data = fixture()
const rows = inventoryFor(buildPlan(data.sources, data.policy))
assert.throws(() => buildPlan(data.sources, data.policy, [...rows, rows[0]]), /重复表/)
assert.throws(() => buildPlan(data.sources, data.policy, [{ ...rows[0], schema: 'other_database' }]), /范围外/)
assert.throws(() => buildPlan(data.sources, data.policy, [{ ...rows[0], table: 'ord_order; DROP TABLE x' }]), /标识符非法/)
assert.throws(() => inspectionSql(["x'); DROP DATABASE x; --"]), /标识符非法/)
})
test('盘点 SQL 只有元数据 SELECT 并包含跨范围外键引用,输出路径不能覆盖源码', () => {
const plan = buildPlan(sources, policy)
const sql = inspectionSql(plan.schemas).split('\n').filter(line => !line.startsWith('--')).join('\n')
const statements = sql.split(';').map(value => value.trim()).filter(Boolean)
assert.equal(statements.length, 5)
assert.ok(statements.every(statement => statement.startsWith('SELECT ')))
assert.match(sql, /OR REFERENCED_TABLE_SCHEMA IN/)
assert.throws(() => writeReport('scripts', plan), /报告目录必须/)
assert.throws(() => writeReport('reports/../scripts/business-reset-test', plan), /报告目录必须/)
})
test('真实 CLI 默认仅打印离线摘要,拒绝 execute 参数', () => {
const script = 'scripts/test-env-business-reset-plan.mjs'
const result = spawnSync(process.execPath, [script], { cwd: repositoryRoot, encoding: 'utf8' })
assert.equal(result.status, 0, result.stderr)
assert.equal(JSON.parse(result.stdout).executionSupported, false)
assert.equal(JSON.parse(result.stdout).output, null)
const execute = spawnSync(process.execPath, [script, '--execute'], { cwd: repositoryRoot, encoding: 'utf8' })
assert.equal(execute.status, 1)
assert.match(execute.stderr, /未知、重复或缺少值/)
})