import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { performance } from 'node:perf_hooks'; import test from 'node:test'; import { BUSINESS_ROUTES, resolveBusinessRoute, resolveRouteFieldLabel } from '../src/business-routes.js'; import { compareParserResults, parserFieldDifferences } from '../src/parser-contract.js'; import { parseProgramInput, programFallbackAllowed } from '../src/program-parser.js'; import { ParserOrchestrator } from '../src/parser-orchestrator.js'; const receivedAt = '2026-08-25T09:00:00+08:00'; const headers = '序号\t姓名\tNAME\t性别\t出生日期\t出生地\t证件类型\t证件号码\t签发地\t签发日\t有效期\t电话\t备注'; const row = '1\t测试游客\tTEST USER\t男\t1990-01-01\t湖南\t护照\tE90000001\t中国\t2025-01-01\t2035-01-01\t13800000001\t合成示例'; const routeCases: Array<{ routeId: string; action: string; input: string }> = [ { routeId: 'team_order_create', action: 'team_order_create', input: `独立团单个下单 发团日期:09-03 预订客户:老挝联泰人名币 产品搜索:遇见老挝 预估人数:15+1 用房数量:8标1单` }, { routeId: 'team_order_batch_create', action: 'team_order_batch_create', input: `独立团批量下单 发团日期:09-01到09-30 预订客户:衡阳广东 产品搜索:老挝好时光 预估人数:15+1 用房数量:8标1单 发团周期:09.03/09.05/09.30` }, { routeId: 'shared_plan_create', action: 'shared_plan_create', input: `散拼团新增计划 发团日期:09-01到09-30 产品搜索:广东衡阳6D 计划收客数:40 用房数量:8标1单 发团周期:09.07/09.15/09.26` }, { routeId: 'shared_child_order_create', action: 'shared_child_order_create', input: `散拼团单个新增子单 预订客户:辽宁康辉 出发日期:2026-09-15 产品名称:老挝广东8D 领队:张三 人数:15+1` }, { routeId: 'passenger_list_import_independent', action: 'passenger_list_import', input: `导入独立团名单 预订客户:辽宁康辉 出发日期:2026-09-03 名单内容: ${headers} ${row}` }, { routeId: 'passenger_list_import_shared_child', action: 'passenger_list_import', input: `导入散拼子单名单 单号:D26091501 名单内容: ${headers} ${row} 覆盖确认:是` }, { routeId: 'arrangement_guide_create', action: 'arrangement_guide', input: `安排导游 团号:LW-260903-A 导游搜索:李明 备注:首日机场接团` }, { routeId: 'arrangement_vehicle_create', action: 'arrangement_vehicle', input: `安排用车 团号:LW-260903-A 用车日期:09-03到09-15 结算单位搜索:380 数量:1` }, { routeId: 'arrangement_hotel_create', action: 'arrangement_hotel', input: `安排酒店 团号:LW-260903-A 入住日期:09-03 离店日期:09-09 酒店搜索:万荣 龙吟阁 间数:8` }, { routeId: 'arrangement_transport_create', action: 'arrangement_transport', input: `安排大交通 团号:LW-260903-A 结算单位搜索:老挝航空 数量:16 交通日期:09-03` }, { routeId: 'arrangement_other_create', action: 'arrangement_other', input: `安排其他/备案 团号:LW-260903-A 业务日期:09-03 结算单位搜索:美女 数量:15` }, { routeId: 'order_update_shared_plan', action: 'order_update_shared_plan', input: `修改散拼母团计划收客数 出发日期:2026-09-15 产品名称:老挝广东8D 目标计划收客数:45` }, { routeId: 'order_update_shared_child', action: 'order_update_shared_child', input: `追加散拼子单订房说明 预订客户:辽宁康辉 出发日期:2026-09-15 追加内容:领队单住` }, { routeId: 'order_update_independent', action: 'order_update_independent', input: `独立团信息修改 预订客户:辽宁康辉 出发日期:2026-09-03 目标修改:人数改为8+1(占)+1;用房数量改为3SGL+2TWN` }, { routeId: 'arrangement_hotel_update', action: 'arrangement_hotel', input: `安排变更 团号:LW-260903-A 目标变更:离店日期改为09-13;房间数改为9间 安排类型:酒店` }, { routeId: 'order_cancel', action: 'order_cancel', input: `取消订单 预订客户:辽宁康辉 出发日期:2026-09-03` }, { routeId: 'order_restore', action: 'order_restore', input: `恢复订单 单号:LW-260903-A 恢复状态:预订` }, { routeId: 'confirmation_export', action: 'confirmation_export', input: `导出团队文件 预订客户:辽宁康辉 出发日期:2026-09-03 文件类型:联泰客户确认单` } ]; test('the machine registry contains the 18 independent routes exactly once', () => { assert.equal(BUSINESS_ROUTES.length, 18); assert.equal(new Set(BUSINESS_ROUTES.map((route) => route.routeId)).size, 18); assert.equal(new Set(BUSINESS_ROUTES.map((route) => route.directive)).size, 18); assert.deepEqual(BUSINESS_ROUTES.map((route) => route.routeId), routeCases.map((route) => route.routeId)); assert.deepEqual( [...BUSINESS_ROUTES].sort((left, right) => left.releaseOrder - right.releaseOrder).map((route) => route.releaseOrder), Array.from({ length: 18 }, (_, index) => index + 1) ); assert.equal(BUSINESS_ROUTES.find((route) => route.releaseOrder === 1)?.routeId, 'confirmation_export'); assert.equal(BUSINESS_ROUTES.find((route) => route.releaseOrder === 18)?.routeId, 'order_update_independent'); }); test('machine routes stay synchronized with the operator template and business registry', async () => { const [template, registry, migration] = await Promise.all([ readFile(new URL('../../agent设计规范/templates/business-input-templates.md', import.meta.url), 'utf8'), readFile(new URL('../../agent设计规范/business-adaptation-registry.md', import.meta.url), 'utf8'), readFile(new URL('../migrations/013_business_parser_modes.sql', import.meta.url), 'utf8') ]); for (const route of BUSINESS_ROUTES) { assert.match(template, new RegExp(route.directive.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), route.routeId); assert.match(registry, new RegExp(route.action.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), route.routeId); assert.match(migration, new RegExp(`'${route.routeId}'`), route.routeId); } }); test('versioned sanitized gold corpus replays deterministically', async () => { const corpus = JSON.parse(await readFile( new URL('../../samples/business-parser-gold-corpus.json', import.meta.url), 'utf8' )); assert.equal(corpus.version, 'ltjt-program-parser-v1.0.6'); assert.ok(corpus.cases.length >= 28); for (const candidate of corpus.cases) { const parse = () => parseProgramInput({ rawText: candidate.input, receivedAt: corpus.received_at, routeId: candidate.route_id, history: (candidate.history || []).map((text: string) => ({ text, receivedAt: corpus.received_at })) }); const [first, second] = await Promise.all([parse(), parse()]); assert.deepEqual(first, second, `${candidate.id} must replay identically`); assert.equal(first.program_parser_version, corpus.version, `${candidate.id} parser version`); assert.equal(first.status, candidate.expected_status, `${candidate.id}: ${JSON.stringify(first)}`); if (candidate.expected_action) assert.equal((first.operation as any)?.action, candidate.expected_action, candidate.id); if (candidate.expected_error_code) assert.equal(first.error_code, candidate.expected_error_code, candidate.id); } }); test('all 18 official-format examples produce contract-valid operations', async () => { for (const candidate of routeCases) { const result = await parseProgramInput({ rawText: candidate.input, receivedAt }); assert.equal(result.business_route_id, candidate.routeId, candidate.routeId); assert.equal(result.status, 'agent_parse_passed', `${candidate.routeId}: ${JSON.stringify(result)}`); assert.equal((result.operation as Record).action, candidate.action, candidate.routeId); } }); test('passenger TSV promotes only an exact leader remark into structured ERP contact data', async () => { const leaderRow = '1\t测试领队\tLEADER TEST\t男\t1980-01-01\t湖南\t护照\tE90000001\t中国\t2025-01-01\t2035-01-01\t13800000001\t 领队 '; const exact = await parseProgramInput({ rawText: routeCases[4].input.replace(row, leaderRow), receivedAt }); assert.equal(exact.status, 'agent_parse_passed', JSON.stringify(exact)); assert.deepEqual((exact.operation as any).data.passenger_list.leader_contact, { sequence: 1, name: '测试领队', phone: '13800000001' }); const ordinaryRemark = await parseProgramInput({ rawText: routeCases[4].input.replace(row, leaderRow.replace('\t 领队 ', '\t领队备注')), receivedAt }); assert.equal(ordinaryRemark.status, 'agent_parse_passed', JSON.stringify(ordinaryRemark)); assert.equal((ordinaryRemark.operation as any).data.passenger_list.leader_contact, undefined); }); test('passenger TSV rejects duplicate leaders or a leader without both name and phone', async () => { const leader = '1\t测试领队甲\tLEADER ONE\t男\t1980-01-01\t湖南\t护照\tE90000001\t中国\t2025-01-01\t2035-01-01\t13800000001\t领队'; const duplicate = '2\t测试领队乙\tLEADER TWO\t女\t1981-01-01\t湖南\t护照\tE90000002\t中国\t2025-01-01\t2035-01-01\t13800000002\t领队'; const duplicated = await parseProgramInput({ rawText: routeCases[4].input.replace(row, `${leader}\n${duplicate}`), receivedAt }); assert.equal(duplicated.status, 'agent_parse_needs_input'); assert.equal(duplicated.error_code, 'program_invalid_value'); assert.match(String(duplicated.reply), /只能标记一位领队/); for (const invalidRow of [ leader.replace('\t测试领队甲\t', '\t\t'), leader.replace('\t13800000001\t领队', '\t\t领队') ]) { const invalid = await parseProgramInput({ rawText: routeCases[4].input.replace(row, invalidRow), receivedAt }); assert.equal(invalid.status, 'agent_parse_needs_input'); assert.equal(invalid.error_code, 'program_invalid_value'); assert.match(String(invalid.reply), /姓名和电话/); } }); test('independent headcount distributions keep occupied and non-occupied children distinct', async () => { const parseHeadcount = (value: string) => parseProgramInput({ rawText: routeCases[13].input.replace( '人数改为8+1(占)+1;用房数量改为3SGL+2TWN', `人数改为${value}` ), receivedAt }); const reported = await parseHeadcount('8+1(占)+1(不占)+1'); assert.equal(reported.status, 'agent_parse_passed', JSON.stringify(reported)); assert.deepEqual((reported.operation as any).data.updates.actions, [ { target: 'pax.adult', operation: 'set', value: 8 }, { target: 'pax.child_bed', operation: 'set', value: 1 }, { target: 'pax.child_no_bed', operation: 'set', value: 1 }, { target: 'pax.leader', operation: 'set', value: 1 } ]); const reversedLabels = await parseHeadcount('8+2(不占)+3(占)+1'); assert.equal(reversedLabels.status, 'agent_parse_passed', JSON.stringify(reversedLabels)); assert.deepEqual( Object.fromEntries((reversedLabels.operation as any).data.updates.actions.map((item: any) => [item.target, item.value])), { 'pax.adult': 8, 'pax.child_no_bed': 2, 'pax.child_bed': 3, 'pax.leader': 1 } ); const legacyPositional = await parseHeadcount('8+1+2+1'); assert.equal(legacyPositional.status, 'agent_parse_passed', JSON.stringify(legacyPositional)); assert.deepEqual((legacyPositional.operation as any).data.updates.actions, [ { target: 'pax.adult', operation: 'set', value: 8 }, { target: 'pax.child_bed', operation: 'set', value: 1 }, { target: 'pax.child_no_bed', operation: 'set', value: 2 }, { target: 'pax.leader', operation: 'set', value: 1 } ]); const taggedWithInfant = await parseHeadcount('8+1(占)+2(不占)+0+1'); assert.equal(taggedWithInfant.status, 'agent_parse_passed', JSON.stringify(taggedWithInfant)); assert.deepEqual((taggedWithInfant.operation as any).data.updates.actions, [ { target: 'pax.adult', operation: 'set', value: 8 }, { target: 'pax.child_bed', operation: 'set', value: 1 }, { target: 'pax.child_no_bed', operation: 'set', value: 2 }, { target: 'pax.infant', operation: 'set', value: 0 }, { target: 'pax.leader', operation: 'set', value: 1 } ]); const legacyNonBed = await parseHeadcount('8+2(不占)+1'); assert.equal(legacyNonBed.status, 'agent_parse_passed', JSON.stringify(legacyNonBed)); assert.deepEqual((legacyNonBed.operation as any).data.updates.actions, [ { target: 'pax.adult', operation: 'set', value: 8 }, { target: 'pax.child_no_bed', operation: 'set', value: 2 }, { target: 'pax.leader', operation: 'set', value: 1 } ]); for (const invalid of [ '8+1(占)+2(占)+1', '8+1(占)+2+1', '+8+1(占)+2(不占)+1', '8++1(占)+2(不占)+1', '8+1(占)+2(不占)+1+', '9007199254740993+1(占)+2(不占)+1', '成人9007199254740993+领队1', '0+0+0+0' ]) { const result = await parseHeadcount(invalid); assert.equal(result.status, 'agent_parse_needs_input', `${invalid}: ${JSON.stringify(result)}`); assert.equal(result.error_code, 'program_invalid_value', invalid); assert.equal(programFallbackAllowed(result), false, invalid); assert.equal(result.operation, null, invalid); assert.equal(result.no_erp_write, true, invalid); assert.equal(result.no_plugin_dispatch, true, invalid); } }); test('registered customer typo is normalized only in name while keyword preserves source', async () => { const result = await parseProgramInput({ rawText: routeCases[0].input, receivedAt }); const customer = ((result.operation as any).data.customer); assert.deepEqual(customer, { name: '老挝联泰人民币', keyword: '老挝联泰人名币' }); }); test('route and field labels allow only a unique one-character correction', async () => { assert.equal(resolveBusinessRoute('导出团队文伴').routeId, 'confirmation_export'); const route = BUSINESS_ROUTES.find((candidate) => candidate.routeId === 'confirmation_export')!; assert.equal(resolveRouteFieldLabel(route, '文件类形').field?.key, 'export_type'); const result = await parseProgramInput({ rawText: `导出团队文伴\n预订客户:辽宁康辉\n出发日期:2026-09-03\n文件类形:联泰客户确认单`, receivedAt }); assert.equal(result.status, 'agent_parse_passed'); }); test('route resolution is source-agnostic for embedded directives and complete unique field signatures', async () => { const wrapped = `AgentBus 转发的结构化业务块\n${routeCases[0].input}`; const wrappedResolution = resolveBusinessRoute(wrapped); assert.equal(wrappedResolution.routeId, 'team_order_create'); assert.equal(wrappedResolution.match, 'exact'); const wrappedProgram = await parseProgramInput({ rawText: wrapped, receivedAt }); assert.equal(wrappedProgram.business_route_id, 'team_order_create'); assert.equal(wrappedProgram.status, 'agent_parse_blocked'); assert.equal(wrappedProgram.error_code, 'program_unsupported_syntax'); const fieldOnly = routeCases[0].input.split('\n').slice(1).join('\n'); const signatureResolution = resolveBusinessRoute(fieldOnly); assert.equal(signatureResolution.routeId, 'team_order_create'); assert.equal(signatureResolution.match, 'signature'); const fieldOnlyProgram = await parseProgramInput({ rawText: fieldOnly, receivedAt }); assert.equal(fieldOnlyProgram.status, 'agent_parse_passed'); assert.equal(fieldOnlyProgram.business_route_id, 'team_order_create'); const ambiguousLookup = `预订客户:辽宁康辉\n出发日期:2026-09-03`; assert.deepEqual( { routeId: resolveBusinessRoute(ambiguousLookup).routeId, match: resolveBusinessRoute(ambiguousLookup).match }, { routeId: null, match: 'ambiguous' } ); assert.equal(resolveBusinessRoute('文件类型:联泰客户确认单').routeId, null); }); test('unknown labels are eligible for auto fallback, but invalid values and conflicts are not', async () => { const unknown = await parseProgramInput({ rawText: `${routeCases[0].input}\n内部编号:123`, receivedAt }); assert.equal(unknown.error_code, 'program_unknown_field'); assert.equal(programFallbackAllowed(unknown), true); const invalid = await parseProgramInput({ rawText: routeCases[0].input.replace('发团日期:09-03', '发团日期:2026-02-30'), receivedAt }); assert.equal(invalid.status, 'agent_parse_needs_input'); assert.equal(invalid.error_code, 'program_invalid_value'); assert.equal(programFallbackAllowed(invalid), false); const conflict = await parseProgramInput({ rawText: `${routeCases[0].input}\n用房数量:9标`, receivedAt }); assert.equal(conflict.status, 'agent_parse_needs_input'); assert.equal(conflict.error_code, 'program_field_conflict'); assert.equal(programFallbackAllowed(conflict), false); }); test('business-value errors stay on the deterministic path instead of becoming contract fallbacks', async () => { const badPassengerSyntax = await parseProgramInput({ rawText: routeCases[0].input.replace('预估人数:15+1', '预估人数:成人15+外星人9'), receivedAt }); assert.equal(badPassengerSyntax.error_code, 'program_invalid_value'); assert.equal(programFallbackAllowed(badPassengerSyntax), false); const oneBatchDate = await parseProgramInput({ rawText: routeCases[1].input.replace('09.03/09.05/09.30', '09.03'), receivedAt }); assert.equal(oneBatchDate.error_code, 'program_invalid_value'); assert.equal(programFallbackAllowed(oneBatchDate), false); const invalidHotelRange = await parseProgramInput({ rawText: routeCases[8].input.replace('离店日期:09-09', '离店日期:09-03'), receivedAt }); assert.equal(invalidHotelRange.error_code, 'program_invalid_value'); assert.equal(programFallbackAllowed(invalidHotelRange), false); const longLodgingNote = await parseProgramInput({ rawText: routeCases[12].input.replace('领队单住', '订房说明'.repeat(20)), receivedAt }); assert.equal(longLodgingNote.error_code, 'program_invalid_value'); assert.equal(programFallbackAllowed(longLodgingNote), false); }); test('independent updates require an exact registered target name', async () => { const unknownTarget = await parseProgramInput({ rawText: routeCases[13].input.replace( '人数改为8+1(占)+1;用房数量改为3SGL+2TWN', '人数备注改为8+1' ), receivedAt }); assert.equal(unknownTarget.error_code, 'program_unsupported_syntax'); assert.equal(programFallbackAllowed(unknownTarget), true); const invalidKnownTarget = await parseProgramInput({ rawText: routeCases[13].input.replace( '人数改为8+1(占)+1;用房数量改为3SGL+2TWN', '行程天数改为0' ), receivedAt }); assert.equal(invalidKnownTarget.error_code, 'program_invalid_value'); assert.equal(programFallbackAllowed(invalidKnownTarget), false); }); test('later turns overwrite prior fields while an unlabeled supplement fills exactly one missing field', async () => { const initial = `安排导游\n团号:LW-260903-A`; const completed = await parseProgramInput({ rawText: '李明', receivedAt, routeId: 'arrangement_guide_create', history: [{ text: initial, receivedAt }] }); assert.equal(completed.status, 'agent_parse_passed'); assert.equal((completed.operation as any).data.arrangement.resource.name, '李明'); const overwritten = await parseProgramInput({ rawText: '导游搜索:王芳', receivedAt, routeId: 'arrangement_guide_create', history: [{ text: `${initial}\n导游搜索:李明`, receivedAt }] }); assert.equal(overwritten.status, 'agent_parse_passed'); assert.equal((overwritten.operation as any).data.arrangement.resource.name, '王芳'); const resolvedConflict = await parseProgramInput({ rawText: '导游搜索:赵敏', receivedAt, routeId: 'arrangement_guide_create', history: [{ text: `${initial}\n导游搜索:李明\n导游搜索:王芳`, receivedAt }] }); assert.equal(resolvedConflict.status, 'agent_parse_passed'); assert.equal((resolvedConflict.operation as any).data.arrangement.resource.name, '赵敏'); }); test('a second business directive cannot be merged into the same task', async () => { const result = await parseProgramInput({ rawText: routeCases[15].input, receivedAt, routeId: 'confirmation_export', history: [{ text: routeCases[17].input, receivedAt }] }); assert.equal(result.error_code, 'program_multiple_actions'); assert.equal(programFallbackAllowed(result), false); const sameTurn = await parseProgramInput({ rawText: `${routeCases[17].input}\n取消订单\n单号:SYNTH-GROUP-001`, receivedAt }); assert.equal(sameTurn.error_code, 'program_multiple_actions'); assert.equal(programFallbackAllowed(sameTurn), false); }); test('passenger TSV rejects duplicate sequence numbers without changing any value', async () => { const duplicate = routeCases[4].input.replace(row, `${row}\n${row}`); const result = await parseProgramInput({ rawText: duplicate, receivedAt }); assert.equal(result.status, 'agent_parse_needs_input'); assert.equal(result.error_code, 'program_invalid_value'); assert.match(String((result.questions as any[])[0].prompt), /序号.*重复/); }); test('all export expands deterministically and pickup sign requires a visitor name', async () => { const all = await parseProgramInput({ rawText: `导出团队文件\n单号:LW-260903-A\n文件类型:全部\n游客姓名:测试游客`, receivedAt }); assert.equal(all.status, 'agent_parse_passed'); assert.equal((all.operation as any).data.export_types.length, 10); const pickup = await parseProgramInput({ rawText: `导出团队文件\n单号:LW-260903-A\n文件类型:接机牌`, receivedAt }); assert.equal(pickup.status, 'agent_parse_needs_input'); assert.deepEqual(pickup.missing_fields, ['visitor_name']); }); test('whole shared-plan visitor information stays distinct from order and child visitor lists', async () => { const wholeGroup = await parseProgramInput({ rawText: `导出团队文件 预订客户:LW衡阳国旅云南分公司 出发日期:2026-11-01 产品名称:广东衡阳 文件类型:整团游客信息`, receivedAt }); assert.equal(wholeGroup.status, 'agent_parse_passed'); assert.deepEqual((wholeGroup.operation as any).data.existing_refs, { kind: 'shared_plan' }); assert.deepEqual((wholeGroup.operation as any).data.confirmation, { type: 'visitor-list' }); const explicitGroup = await parseProgramInput({ rawText: `导出团队文件 单号:LW-261101A-A 文件类型:整团游客信息`, receivedAt }); assert.equal(explicitGroup.status, 'agent_parse_passed'); assert.deepEqual((explicitGroup.operation as any).data.existing_refs, { kind: 'shared_plan', identifier: 'LW-261101A-A' }); const concreteOrder = await parseProgramInput({ rawText: `导出团队文件 单号:D26110101 文件类型:游客名单`, receivedAt }); assert.equal(concreteOrder.status, 'agent_parse_passed'); assert.deepEqual((concreteOrder.operation as any).data.existing_refs, { identifier: 'D26110101' }); assert.deepEqual((concreteOrder.operation as any).data.confirmation, { type: 'visitor-list' }); const mixedScope = await parseProgramInput({ rawText: `导出团队文件 单号:LW-261101A-A 文件类型:整团游客信息/游客名单`, receivedAt }); assert.equal(mixedScope.status, 'agent_parse_needs_input'); assert.equal(mixedScope.error_code, 'program_invalid_value'); assert.deepEqual(mixedScope.missing_fields, ['export_type']); assert.match(String((mixedScope.questions as any[])[0].prompt), /整团游客信息必须单独导出/); }); test('date normalization handles next occurrence, cross-year ranges, and leap days', async () => { const crossYear = await parseProgramInput({ rawText: `独立团批量下单 发团日期:12-28到01-05 预订客户:合成客户 产品搜索:合成产品 预估人数:15+1 用房数量:8标1单 发团周期:12.29/01.03`, receivedAt: '2026-12-20T09:00:00+08:00' }); assert.equal(crossYear.status, 'agent_parse_passed'); assert.deepEqual((crossYear.operation as any).data.recurrence, { start_date: '2026-12-28', end_date: '2027-01-05', pattern: 'specified_dates' }); assert.deepEqual((crossYear.operation as any).data.departure_dates, ['2026-12-29', '2027-01-03']); const nextYear = await parseProgramInput({ rawText: routeCases[0].input.replace('09-03', '01-03'), receivedAt: '2026-12-20T09:00:00+08:00' }); assert.equal((nextYear.operation as any).data.departure_dates[0], '2027-01-03'); const leap = await parseProgramInput({ rawText: routeCases[0].input.replace('09-03', '2028-02-29'), receivedAt }); assert.equal(leap.status, 'agent_parse_passed'); }); test('modification dates use the next occurrence and preserve explicit years', async () => { const reported = await parseProgramInput({ rawText: `追加散拼子单订房说明 预订客户:辽宁康辉 出发日期:11-15 产品名称:广东衡阳 领队:张三 追加内容:领队单住`, receivedAt: '2026-08-26T01:22:50.000Z' }); assert.equal(reported.status, 'agent_parse_passed'); assert.equal((reported.operation as any).data.departure_dates[0], '2026-11-15'); const crossed = await parseProgramInput({ rawText: routeCases[12].input.replace('2026-09-15', '01-15'), receivedAt: '2026-08-26T01:22:50.000Z' }); assert.equal((crossed.operation as any).data.departure_dates[0], '2027-01-15'); const explicit = await parseProgramInput({ rawText: routeCases[12].input.replace('2026-09-15', '2025-11-15'), receivedAt: '2026-08-26T01:22:50.000Z' }); assert.equal((explicit.operation as any).data.departure_dates[0], '2025-11-15'); const targetDate = await parseProgramInput({ rawText: `独立团信息修改 单号:LW-260903-A 目标修改:出发日期改为01-07`, receivedAt: '2026-08-26T01:22:50.000Z' }); assert.equal((targetDate.operation as any).data.updates.actions[0].value, '2027-01-07'); const hotelChange = await parseProgramInput({ rawText: `安排变更 团号:LW-260903-A 目标变更:离店日期改为01-07;房间数改为9间 安排类型:酒店`, receivedAt: '2026-08-26T01:22:50.000Z' }); assert.equal((hotelChange.operation as any).data.arrangement.changes.end_date, '2027-01-07'); }); test('a yearless modification date supplied on a later turn uses that turn date context', async () => { const result = await parseProgramInput({ rawText: '11-15', receivedAt: '2026-08-26T01:23:59.000Z', routeId: 'order_update_shared_child', history: [{ text: '追加散拼子单订房说明\n预订客户:辽宁康辉\n追加内容:领队单住', receivedAt: '2026-08-26T01:22:50.000Z' }] }); assert.equal(result.status, 'agent_parse_passed'); assert.equal((result.operation as any).data.departure_dates[0], '2026-11-15'); }); test('cancel and restore dates use the next occurrence and preserve explicit years', async () => { const restore = await parseProgramInput({ rawText: `恢复订单 预订客户:衡阳国旅广东 出发日期:11-05 产品名称:老挝好时光 恢复状态:预订`, receivedAt: '2026-08-26T05:09:53.000Z' }); assert.equal(restore.status, 'agent_parse_passed'); assert.equal((restore.operation as any).action, 'order_restore'); assert.equal((restore.operation as any).data.departure_dates[0], '2026-11-05'); const cancel = await parseProgramInput({ rawText: `取消订单 预订客户:衡阳国旅广东 出发日期:01-05`, receivedAt: '2026-08-26T05:09:53.000Z' }); assert.equal(cancel.status, 'agent_parse_passed'); assert.equal((cancel.operation as any).data.departure_dates[0], '2027-01-05'); const explicit = await parseProgramInput({ rawText: `恢复订单 预订客户:衡阳国旅广东 出发日期:2025-11-05`, receivedAt: '2026-08-26T05:09:53.000Z' }); assert.equal(explicit.status, 'agent_parse_passed'); assert.equal((explicit.operation as any).data.departure_dates[0], '2025-11-05'); }); test('deterministic parser meets the ordinary and large-list local P95 budgets', async () => { await parseProgramInput({ rawText: routeCases[0].input, receivedAt }); const ordinary: number[] = []; for (let index = 0; index < 40; index += 1) { const started = performance.now(); const result = await parseProgramInput({ rawText: routeCases[0].input, receivedAt }); assert.equal(result.status, 'agent_parse_passed'); ordinary.push(performance.now() - started); } const largeRows = Array.from({ length: 500 }, (_, index) => { const sequence = index + 1; return `${sequence}\t测试游客${sequence}\tTEST ${sequence}\t男\t1990-01-01\t湖南\t护照\tE${String(sequence).padStart(8, '0')}\t中国\t2025-01-01\t2035-01-01\t138${String(sequence).padStart(8, '0')}\t合成示例`; }).join('\n'); const largeInput = `导入独立团名单\n单号:SYNTH-GROUP-001\n名单内容:\n${headers}\n${largeRows}`; const large: number[] = []; for (let index = 0; index < 12; index += 1) { const started = performance.now(); const result = await parseProgramInput({ rawText: largeInput, receivedAt }); assert.equal(result.status, 'agent_parse_passed'); large.push(performance.now() - started); } const p95 = (values: number[]) => [...values].sort((left, right) => left - right)[Math.ceil(values.length * 0.95) - 1]; assert.ok(p95(ordinary) <= 100, `ordinary P95 ${p95(ordinary).toFixed(2)}ms`); assert.ok(p95(large) <= 250, `large-list P95 ${p95(large).toFixed(2)}ms`); }); test('semantic comparator ignores safe ordering but reports changed operation fields', () => { const left = { status: 'agent_parse_passed', operation: { action: 'order_update_independent', data: { updates: { actions: [ { target: 'rooms.TWN', operation: 'set', value: 2 }, { target: 'rooms.SGL', operation: 'set', value: 3 } ] } } } }; const equivalent = structuredClone(left); (equivalent.operation.data.updates.actions as any[]).reverse(); assert.equal(compareParserResults(left, equivalent).status, 'equivalent'); (equivalent.operation.data.updates.actions as any[])[0].value = 99; assert.equal(compareParserResults(left, equivalent).status, 'different'); const details = parserFieldDifferences(left, equivalent); assert.equal(details.length, 1); assert.match(details[0].path, /^\$\.operation/); assert.notEqual(details[0].ai.value, details[0].program.value); }); function orchestrationClaim(mode: 'ai' | 'shadow' | 'auto' | 'program', rawText: string, affinity: 'ai' | 'program' | null = null): any { return { task: { task_id: 'TASK-PARSER-1', created_at: receivedAt, parser: { route_id: 'team_order_create', input_contract_version: 'test-contract-v1', configured_mode: mode, config_revision: 1, authoritative_engine: null, engine_affinity: affinity, version: null, fallback_reason: null, comparison_status: null, decision_id: null, review_status: null, diff_paths: [], field_differences: [], unreviewed_difference_count: 0, shadow_differences: [], reparse_count: 0 } }, attemptNo: 1, workerId: 'worker', messageId: 'message', messageText: rawText, turnNo: 1, sessionId: '', recoveryCount: 0, history: [] }; } test('mode orchestration keeps AI authority in Shadow and enforces the Auto fallback allowlist', async () => { let aiCalls = 0; const aiResult = await parseProgramInput({ rawText: routeCases[0].input, receivedAt }); const ai = { async parse() { aiCalls += 1; return { ...aiResult, parse_mode: 'ai', parser_prompt_version: 'test-ai-v1' }; } }; const orchestrator = new ParserOrchestrator(ai); const shadow = await orchestrator.parse(orchestrationClaim('shadow', routeCases[0].input)); assert.equal(shadow.decision.authoritativeEngine, 'ai'); assert.equal(shadow.decision.comparisonStatus, 'equivalent'); assert.equal(aiCalls, 1); const autoPassed = await orchestrator.parse(orchestrationClaim('auto', routeCases[0].input)); assert.equal(autoPassed.decision.authoritativeEngine, 'program'); assert.equal(aiCalls, 1); const unknownField = `${routeCases[0].input}\n内部编号:123`; const autoFallback = await orchestrator.parse(orchestrationClaim('auto', unknownField)); assert.equal(autoFallback.decision.authoritativeEngine, 'ai'); assert.equal(autoFallback.decision.fallbackReason, 'program_unknown_field'); assert.equal(aiCalls, 2); const programBlocked = await orchestrator.parse(orchestrationClaim('program', unknownField)); assert.equal(programBlocked.decision.authoritativeEngine, 'program'); assert.equal((programBlocked.result as any).error_code, 'program_unknown_field'); assert.equal(aiCalls, 2); const invalidAuto = await orchestrator.parse(orchestrationClaim( 'auto', routeCases[0].input.replace('发团日期:09-03', '发团日期:2026-02-30') )); assert.equal(invalidAuto.decision.authoritativeEngine, 'program'); assert.equal((invalidAuto.result as any).error_code, 'program_invalid_value'); assert.equal(aiCalls, 2); const affinity = await orchestrator.parse(orchestrationClaim('auto', unknownField, 'ai')); assert.equal(affinity.decision.authoritativeEngine, 'ai'); assert.equal(aiCalls, 3); });