import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import test from 'node:test' async function readJobSource() { return readFile(new URL('./data-retention-job.ts', import.meta.url), 'utf8') } test('保留清理必须覆盖 work_order_events,否则这张只增不减的表会一直拖慢历史单检索', async () => { const src = await readJobSource() assert.ok( src.includes("deleteOldRowsInBatches('work_order_events'"), 'work_order_events 未纳入保留清理(生产上 71 万行 / 269MB 且只增不减)', ) assert.ok( src.includes("'task_events' | 'webhook_events' | 'admin_audit_logs' | 'work_order_events'"), '删除助手需要显式允许 work_order_events(表名是白名单联合类型,避免拼接注入)', ) assert.ok(src.includes('retention.workOrderEventDays'), '保留天数必须走配置项,便于按环境调整') assert.ok( src.includes('workOrderEventRows: number') && src.includes('workOrderEventRows,'), '清理结果要带上工单事件的删除行数,方便定时任务面板核对', ) }) test('成本异常只归档已处理的行,open 队列不能被藏起来', async () => { const job = await readJobSource() assert.ok( job.includes('archiveResolvedCostExceptions'), '成本异常必须走归档,不能直接 DELETE 掉审计痕迹', ) assert.ok(job.includes('retention.costExceptionDays'), '保留天数必须走配置项') // 归档 SQL 在独立服务里,检查服务源码而不是任务源码。 const service = await readFile( new URL('../cost-center/cost-exception-archive-service.ts', import.meta.url), 'utf8', ) assert.ok(service.includes("status <> 'open'"), 'open 行是待办队列,不能因超期被归档') assert.ok( service.includes('COALESCE(resolved_at, created_at)'), '归档时钟优先用 resolved_at,ignored 行没有 resolved_at 时回退 created_at', ) assert.ok( service.includes('INSERT INTO cost_exceptions_archive'), '归档必须落到 cost_exceptions_archive,保持可追溯', ) }) test('保留任务要回填漏写的付款派生列,并在有命中时打日志', async () => { const src = await readJobSource() assert.ok( src.includes('source_order_amount_fen IS NULL'), '只处理派生列为空的行,避免把合法 0 或已落列的金额改掉', ) assert.ok( src.includes("material_json->'source'->>'orderAmountFen'"), '回填口径必须与 083 / resolveSourceOrderAmountFen 相同', ) assert.ok(src.includes("~ '^-?[0-9]{1,15}$'"), '超长数字不能 ::bigint,否则整次保留任务失败') assert.ok( src.includes('source_order_amount_fen 出现漏写'), '有命中必须打日志,否则金额静默回退 orders.total_amount', ) }) test('保留清理默认窗口:敏感报文/事件 180 天,审计 365 天,工单事件 180 天', async () => { const defaults = await readFile(new URL('../../config/defaults.ts', import.meta.url), 'utf8') for (const line of [ 'rawPayloadDays: 180', 'taskEventDays: 180', 'webhookEventDays: 180', 'auditLogDays: 365', 'workOrderEventDays: 180', 'costExceptionDays: 60', ]) { assert.ok(defaults.includes(line), `默认保留策略缺少 ${line}`) } const overrides = await readFile( new URL('../../config/env-overrides.ts', import.meta.url), 'utf8', ) assert.ok( overrides.includes( "integerEnv('WORK_ORDER_EVENT_RETENTION_DAYS', ['retention', 'workOrderEventDays'])", ), 'WORK_ORDER_EVENT_RETENTION_DAYS 需要环境变量出口,否则生产无法调整', ) assert.ok( overrides.includes( "integerEnv('COST_EXCEPTION_RETENTION_DAYS', ['retention', 'costExceptionDays'])", ), 'COST_EXCEPTION_RETENTION_DAYS 需要环境变量出口,否则生产无法调整归档窗口', ) })