Files
LWLT-AIBOT/control-plane/test/program-parser.test.ts
T

757 lines
33 KiB
TypeScript

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<string, unknown>).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);
});