import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import test from 'node:test' import ts from 'typescript' import { CONSIGNMENT_MATCH_REASON_LABELS, resolveConsignmentMatchReasonLabel, } from '../src/components/admin/consignment-match-log-labels.ts' const ENGINE_PATH = fileURLToPath( new URL( '../../backend/src/services/worker-platform/consignment-match-engine-service.ts', import.meta.url, ), ) const MODAL_PATH = fileURLToPath( new URL('../src/components/admin/ConsignmentMatchLogsModal.tsx', import.meta.url), ) function collectStringLiterals(expr: ts.Expression, result: Set) { if (ts.isStringLiteralLike(expr)) { result.add(expr.text) return } if (ts.isParenthesizedExpression(expr)) { collectStringLiterals(expr.expression, result) return } if (ts.isConditionalExpression(expr)) { // 只取分支结果,不把条件里的字符串当成原因码。 collectStringLiterals(expr.whenTrue, result) collectStringLiterals(expr.whenFalse, result) return } if ( ts.isBinaryExpression(expr) && [ ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.PlusToken, ].includes(expr.operatorToken.kind) ) { collectStringLiterals(expr.left, result) collectStringLiterals(expr.right, result) return } } /** 匹配日志的 reasonCode 只有两种来源:固定流程原因 + createHttpError 的 errorCode。 */ function collectEngineReasonCodes() { const source = readFileSync(ENGINE_PATH, 'utf8') const sourceFile = ts.createSourceFile(ENGINE_PATH, source, ts.ScriptTarget.Latest, true) const result = new Set() function visit(node: ts.Node) { if (ts.isCallExpression(node)) { const callee = node.expression.getText(sourceFile).split('.').pop() || '' if (callee === 'recordConsignmentMatchLog') { const objectArg = node.arguments.find((arg): arg is ts.ObjectLiteralExpression => ts.isObjectLiteralExpression(arg), ) for (const property of objectArg?.properties || []) { if ( ts.isPropertyAssignment(property) && property.name.getText(sourceFile) === 'reasonCode' ) { collectStringLiterals(property.initializer, result) } } } } if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === 'errorCode') { collectStringLiterals(node.initializer, result) } ts.forEachChild(node, visit) } visit(sourceFile) return result } test('寄卖匹配日志:后端写入的每个原因码都必须有中文映射', () => { const reasonCodes = collectEngineReasonCodes() assert.ok(reasonCodes.size > 0, '没有扫描到任何原因码,测试逻辑可能失效') const missing = [...reasonCodes].filter((code) => !CONSIGNMENT_MATCH_REASON_LABELS[code]).sort() assert.deepEqual( missing, [], `以下原因码缺少中文映射,会在匹配日志里直接显示英文:${missing.join(', ')}`, ) }) test('寄卖匹配日志:原因列优先显示中文,未知码兜底原文', () => { assert.equal(resolveConsignmentMatchReasonLabel('manual_matched', ''), '手动匹配成功') assert.equal(resolveConsignmentMatchReasonLabel('manual_matched', undefined), '手动匹配成功') assert.equal( resolveConsignmentMatchReasonLabel( 'consignment_spec_mismatch', '所选库存与订单规格不一致:INV1', ), '所选库存与订单规格不一致:INV1', ) assert.equal(resolveConsignmentMatchReasonLabel('future_unknown_code', ''), 'future_unknown_code') assert.equal(resolveConsignmentMatchReasonLabel('', ''), '-') }) test('寄卖匹配日志弹窗:工单右侧新增寄卖单号列', () => { const modalSource = readFileSync(MODAL_PATH, 'utf8') assert.ok(modalSource.includes("title: '寄卖单号'"), '必须新增「寄卖单号」列') assert.ok( modalSource.includes("dataIndex: 'consignmentNos'"), '「寄卖单号」列必须读取后端返回的 consignmentNos', ) assert.ok( modalSource.indexOf("title: '寄卖单号'") > modalSource.indexOf("title: '工单'"), '「寄卖单号」列必须在「工单」列右侧', ) assert.ok(modalSource.includes('resolveConsignmentMatchReasonLabel'), '原因列必须使用中文映射') })