import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { validateStandardOperation } from '../LianSyn-platform/external-agent-client.mjs'; const require = createRequire(import.meta.url); const plans = require('../chrome-extension/ltjt-order-assistant/operation-plans.js'); const baseCreate = { action: 'team_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: { customer: { name: '老挝联泰人民币', keyword: '老挝联泰人名币', source_region: '示例来源' }, product: { name: '遇见老挝', source_region: '示例来源' }, departure_dates: ['2026-08-11'], passenger_counts: { adult: 2, expected_total: 2 }, room_counts: { DBL: 1 }, }, }; test('team-single remains the guarded production team-order path', () => { const result = plans.validateOperation(baseCreate); assert.equal(result.ok, true); assert.equal(result.action, 'team_order_create'); assert.equal(result.execution, 'browser_team_single'); assert.equal(result.noErpWrite, false); }); test('accepts product/customer source-region context without semantic validation', () => { const result = plans.validateOperation({ ...baseCreate, data: { ...baseCreate.data, customer: { name: '广西来源客户', source_region: '广西' }, product: { name: '广东来源产品', source_region: '广东' } } }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_team_single'); assert.doesNotMatch(result.blockers.join('\n'), /来源地/); }); test('accepts a team-single operation without source-region metadata', () => { const result = plans.validateOperation({ ...baseCreate, data: { customer: { name: '老挝联泰人民币', keyword: '老挝联泰人名币' }, product: { name: '遇见老挝', keyword: '遇见老挝' }, departure_dates: ['2026-08-11'], passenger_counts: { adult: 2, expected_total: 2 }, room_counts: { DBL: 1 } } }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_team_single'); }); test('formal team-batch uses the validated native batch submit path', () => { const operation = { ...baseCreate, action: 'team_order_batch_create', data: { ...baseCreate.data, customer: { name: '衡阳广东', keyword: '衡阳广东', source_region: '示例来源' }, departure_dates: ['2026-08-11', '2026-08-18'], recurrence: { start_date: '2026-08-01', end_date: '2026-08-31', pattern: 'specified_dates' }, }, }; const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_team_batch_native'); assert.equal(result.noErpWrite, false); assert.match(result.warnings.join('\n'), /原生.*DoInfoJHs/); const expanded = plans.expandTeamBatch(operation); assert.equal(expanded.length, 2); assert.deepEqual(expanded.map((item) => item.data.departure_dates[0]), ['2026-08-11', '2026-08-18']); assert.equal(Object.hasOwn(expanded[0].data, 'test_marker'), false); assert.equal(expanded.every((item) => item.source === undefined), true); }); test('order updates require an identifier and only run a no-write browser preview', () => { const result = plans.validateOperation({ action: 'order_update', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'LW-270520A-A' }, updates: { actions: [{ target: 'rooms.TWN', operation: 'set', value: 10 }], }, }, }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_dry_run'); assert.equal(result.noErpWrite, true); assert.equal(result.identifier, 'LW-270520A-A'); }); test('untyped parent confirmation export is blocked before any browser operation', () => { const result = plans.validateOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { parent_group_no: 'PARENT-EXAMPLE' } }, }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /existing_refs\.kind|缺少标准确认件类型|唯一解析/); assert.equal(result.execution, 'blocked'); }); test('confirmation export accepts canonical types and blocks unknown artifacts', () => { const dispatch = plans.validateDispatchOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'LW-270520A-A' }, export_types: ['xingyou-confirm', 'job-order'] }, }); assert.equal(dispatch.ok, true); assert.equal(dispatch.requiresErpResolution, true); assert.deepEqual(dispatch.exportTypes, ['xingyou-confirm', 'job-order']); const invalid = plans.validateDispatchOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-270520A-A' }, export_types: ['visitor-detail'] }, }); assert.equal(invalid.ok, false); assert.match(invalid.blockers.join('\n'), /不支持/); }); test('unknown operations fail closed', () => { const result = plans.validateOperation({ action: 'delete_order', data: {} }); assert.equal(result.ok, false); assert.equal(result.execution, 'blocked'); assert.match(result.blockers.join('\n'), /不支持/); }); test('rejects legacy create envelopes instead of normalizing them', () => { const operation = plans.normalizeOperation({ operation: 'create_order', route: 'team_single', fields: { productName: '遇见老挝', departureDate: '2026-08-11', pax: 4, op: '测试OP', salesperson: '测试销售', }, originalText: '交接包格式任务', }); const result = plans.validateOperation(operation); assert.equal(operation.action, undefined); assert.equal(result.ok, false); assert.equal(result.execution, 'blocked'); }); test('routes the canonical split-parent operation without child information into the guarded production browser adapter', () => { const operation = { action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '示例线路' }, departure_dates: ['2026-08-15'], planned_capacity: 20, room_counts: { TWN: 8, SGL: 1 }, groups_per_date: 1, }, }; const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_live_split_parent'); assert.equal(result.definition.liveSubmit, 'guarded-production'); assert.equal(operation.data.planned_capacity, 20); }); test('accepts a canonical split-child parent reference without fabricating a product', () => { const operation = { action: 'shared_child_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { parent_group_no: 'PARENT-EXAMPLE' }, departure_dates: ['2026-08-15'], customer: { name: '测试渠道' }, passenger_counts: { adult: 8 }, }, }; const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.equal(operation.data.existing_refs.parent_group_no, 'PARENT-EXAMPLE'); assert.equal(result.execution, 'browser_live_split_child'); }); test('shared-child leader lookup survives the strict execution gate', () => { const operation = { action: 'shared_child_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'shared_plan', identifier: 'LW-260907A-B', parent_group_no: 'LW-260907A-B' }, departure_dates: ['2026-09-07'], customer: { name: '衡阳国旅', keyword: '衡阳国旅' }, product: { name: '衡阳', keyword: '衡阳' }, leader: { name: '张三', keyword: '张三' }, passenger_counts: { adult: 15, leader: 1 }, resolution: { stage: 'erp_readonly', target: 'unique', candidate_count: 1, target_kind: 'shared_plan', write_attempted: false } } }; assert.equal(plans.validateOperation(operation).ok, true, plans.validateOperation(operation).blockers.join('; ')); assert.deepEqual(validateStandardOperation(operation), []); }); test('blocks a marked split-child parent when the child remark loses the test marker', () => { const result = plans.validateOperation({ action: 'shared_child_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { parent_group_no: 'LW-260916TEST-202609-R0511-A', tid: '14389' }, departure_dates: ['2026-09-16'], customer: { name: '广东客户' }, passenger_counts: { adult: 15, leader: 1 } } }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /special_requests.*TEST-202609/); }); test('routes a traveler-only handoff update into the traveler import business action', () => { const operation = plans.normalizeOperation({ action: 'passenger_list_import', order_nature: 'formal', submit_mode: 'dry_run', source: { test_context: { run_id: 'sept-2026-test', marker: 'TEST-202609', account: 'AI_TEST_ACCOUNT', native_baseline_id: 'baseline-001', live_window: '2026-09' } }, data: { existing_refs: { kind: 'independent_order', identifier: 'LW-270520A-A', tid: '270520', ddid: '270521', departure_date: '2026-09-20', owner_account: 'AI_TEST_ACCOUNT', marker: 'TEST-202609', expected_passenger_count: 16 }, passenger_list: { operation: 'first_import', row_count: 16, marker: 'TEST-202609', rows: Array.from({ length: 16 }, (_, index) => Object.fromEntries(plans.PASSENGER_HEADERS.map((header) => [ header, header === '序号' ? index + 1 : (header === '备注' ? 'TEST-202609' : (header === '证件类型' ? '护照' : `${header}-${index + 1}`)) ]))) } }, }); const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_lifecycle'); assert.equal(result.definition.testOnly, undefined); }); test('applies room and remark updates to a browser-read snapshot without writing ERP', () => { const preview = plans.buildUpdatePreview({ action: 'order_update', identifier: 'LW-270520A-A', data: { updates: { actions: [ { target: 'rooms.TWN', operation: 'delta', value: 2 }, { target: 'remark', operation: 'append', value: '客户补充备注' }, ], }, }, }, { rooms: { TWN: 3 }, pax: { adult: 4 }, prices: { adult: 100 }, remark: '原备注', }); assert.equal(preview.status, 'ready'); assert.equal(preview.snapshot.rooms.TWN, 5); assert.match(preview.snapshot.remark, /原备注/); assert.match(preview.snapshot.remark, /客户补充备注/); assert.equal(preview.no_erp_write, true); }); test('builds export-only source paths and never emits a save action', () => { const plan = plans.buildConfirmationExportPlan({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'LW-270520A-A' }, export_types: ['xingyou-confirm', 'liantai-confirm', 'job-order'] }, }, { ddid: '123', tid: '456' }); assert.equal(plan.export_only, true); assert.equal(plan.never_resave, true); assert.equal(plan.delivery_scope, 'backend_task_attachment'); assert.deepEqual(plan.types, ['xingyou-confirm', 'liantai-confirm', 'job-order']); assert.deepEqual(plan.artifacts.map((item) => item.path), [ '/System/Business/orders_confirm_news.asp', '/System/Business/orders_confirm_new.asp', '/System/Business/teams_beian1.asp', ]); assert.equal(plan.no_erp_write, true); }); test('accepts the production team-single output without manual prices or staff fields', () => { const result = plans.validateOperation({ action: 'team_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: { customer: { name: '老挝联泰人民币', keyword: '老挝联泰人名币', source_region: '示例来源' }, product: { name: '遇见老挝', source_region: '示例来源' }, departure_dates: ['2026-08-11'], passenger_counts: { adult: 15, leader: 1 }, room_counts: { TWN: 8, SGL: 1 }, }, }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_team_single'); assert.equal(result.noErpWrite, false); }); test('requires the booking customer for independent-team single orders', () => { const result = plans.validateOperation({ action: 'team_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '遇见老挝' }, departure_dates: ['2026-09-03'], passenger_counts: { adult: 15, leader: 1 }, room_counts: { TWN: 8, SGL: 1 }, }, }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /data\.customer.*对象/); }); test('blocks scalar named references, unsupported room keys, and manual staff/price overrides', () => { const result = plans.validateOperation({ action: 'team_order_batch_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: '老挝好时光', customer: '衡阳广东', departure_dates: ['2026-08-03', '2026-08-05'], passenger_counts: { adult: 15, leader: 1 }, room_counts: { TWN: 8, HNM: 1 }, op_user: { name: '不应由 Agent 提供' }, prices: { adult: 100 }, }, }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /data\.product.*对象/); assert.match(result.blockers.join('\n'), /data\.customer.*对象/); assert.match(result.blockers.join('\n'), /room_counts.*SGL/); assert.match(result.blockers.join('\n'), /op_user/); assert.match(result.blockers.join('\n'), /prices/); }); test('accepts the corrected formal native batch contract used by the reported task', () => { const result = plans.validateOperation({ action: 'team_order_batch_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝好时光', source_region: '广东' }, customer: { name: '衡阳广东', source_region: '广东' }, departure_dates: ['2026-08-03', '2026-08-05', '2026-09-30'], recurrence: { start_date: '2026-08-01', end_date: '2026-09-30' }, passenger_counts: { adult: 15, leader: 1 }, room_counts: { TWN: 8, SGL: 1 } } }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_team_batch_native'); assert.equal(result.noErpWrite, false); }); test('keeps the formal native batch operation free of test markers', () => { const operation = { action: 'team_order_batch_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '遇见老挝', source_region: '示例来源' }, customer: { name: '测试客户', source_region: '示例来源' }, departure_dates: ['2026-08-11', '2026-08-18'], recurrence: { start_date: '2026-08-01', end_date: '2026-08-31' }, passenger_counts: { adult: 15, leader: 1 }, room_counts: { TWN: 8 }, }, }; const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_team_batch_native'); assert.equal(Object.hasOwn(operation.data, 'test_marker'), false); }); test('accepts a split-parent recurrence without child customer or passenger fields', () => { const result = plans.validateOperation({ action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝散拼', source_region: '示例来源' }, recurrence: { start_date: '2026-08-15', end_date: '2026-09-15', pattern: 'weekly' }, planned_capacity: 20, room_counts: { TWN: 8 }, }, }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_live_split_parent'); }); test('accepts independently supplied optional split-parent customer and passenger fields', () => { const result = plans.validateOperation({ action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝散拼', source_region: '示例来源' }, departure_dates: ['2026-08-15'], planned_capacity: 20, room_counts: { TWN: 8 }, split_order: { customer: { name: '示例客户', source_region: '示例来源' }, passenger_counts: { adult: 10 } } }, }); assert.equal(result.ok, true); assert.equal(result.execution, 'browser_live_split_parent'); }); test('accepts either optional split-parent field without requiring the other', () => { const customerOnly = plans.validateOperation({ action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝广东8D' }, departure_dates: ['2026-09-03'], planned_capacity: 30, room_counts: { TWN: 8 }, split_order: { customer: { name: '南宁国旅', source_region: '广东' } } } }); const passengerOnly = plans.validateOperation({ action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝广东8D' }, departure_dates: ['2026-09-03'], planned_capacity: 30, room_counts: { TWN: 8 }, split_order: { passenger_counts: { adult: 15, leader: 1 } } } }); assert.equal(customerOnly.ok, true); assert.equal(passengerOnly.ok, true); }); test('blocks multi-date split facts outside the dedicated live test probe', () => { const operation = { action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝广东8D', source_region: '广东' }, departure_dates: ['2026-09-03', '2026-09-10', '2026-09-17'], planned_capacity: 30, room_counts: { TWN: 8 }, split_order: { customer: { name: '广东测试客户', source_region: '广东' }, passenger_counts: { adult: 15, leader: 1 } } } }; const result = plans.validateOperation(operation); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /尚未完成逐日期真实验证|事实探针/); }); test('allows multi-date split facts only inside the dedicated TEST-202609 probe', () => { const dates = ['2026-09-03', '2026-09-10', '2026-09-17']; const operation = { action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', source: { test_context: { run_id: 'sept-2026-lifecycle-plugin-e2e3-01', account: '测试ai员工账号', marker: 'TEST-202609', native_baseline_id: 'native-shared-batch-split-e2e3', live_window: '2026-09', target_dates: dates, phase: 'shared_batch_split_order_probe' } }, data: { product: { name: '老挝广东8D', source_region: '广东' }, departure_dates: dates, planned_capacity: 30, room_counts: { TWN: 8 }, split_order: { customer: { name: '广东测试客户', source_region: '广东' }, passenger_counts: { adult: 15, leader: 1 } } } }; const result = plans.validateOperation(operation); assert.equal(result.ok, true, result.blockers.join('; ')); assert.match(result.warnings.join('\n'), /测试态事实探针/); const publicPlan = plans.publicPlan(result); assert.equal(publicPlan.contract_version, 'ltjt-lifecycle-v2.9-roster-leader-contact-2026-08'); assert.equal(publicPlan.capability_state, 'validated_parent_create_batch_split_order_probe_only'); }); test('blocks invalid values for an optional split-parent field', () => { const result = plans.validateOperation({ action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝广东8D' }, departure_dates: ['2026-09-03'], planned_capacity: 30, room_counts: { TWN: 8 }, split_order: { passenger_counts: { adult: 0 } } } }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /人数合计必须大于 0/); }); test('normalizes legacy flat split-parent fields into the canonical optional envelope', () => { const operation = plans.normalizeOperation({ action: 'shared_plan_create', order_nature: 'formal', submit_mode: 'dry_run', data: { product: { name: '老挝广东8D' }, departure_dates: ['2026-09-03'], planned_capacity: 30, room_counts: { TWN: 8, SGL: 1 }, customer: { name: '南宁国旅', source_region: '广东' }, passenger_counts: { adult: 15, leader: 1 }, }, }); const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.deepEqual(operation.data.split_order, { customer: { name: '南宁国旅', source_region: '广东' }, passenger_counts: { adult: 15, leader: 1 } }); assert.equal(Object.hasOwn(operation.data, 'customer'), false); assert.equal(Object.hasOwn(operation.data, 'passenger_counts'), false); }); test('accepts canonical confirmation types and snake-case recovery flags', () => { const result = plans.validateOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001', tid: '101', ddid: '102', resolved: true, resolution_source: 'erp_unique_match' }, confirmation: { type: 'xingyou-confirm' }, recovery: { export_only: true, never_resave: true }, }, }); assert.equal(result.ok, true); assert.deepEqual(result.exportTypes, ['xingyou-confirm']); }); test('accepts canonical confirmation export types without alias normalization', () => { const operation = plans.normalizeOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001', tid: '101', ddid: '102', resolved: true, resolution_source: 'erp_unique_match' }, export_types: ['liantai-confirm'], }, }); const result = plans.validateOperation(operation); assert.equal(result.ok, true); assert.deepEqual(result.exportTypes, ['liantai-confirm']); }); test('shared-plan visitor export uses tid only and rejects child-scoped or mixed sources', () => { const operation = { action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'shared_plan', identifier: 'LW-261101A-A', parent_group_no: 'LW-261101A-A', plan_no: 'LW-261101A-A', tid: '456', resolved: true, resolution_source: 'erp_unique_match' }, confirmation: { type: 'visitor-list' } } }; const result = plans.validateOperation(operation); assert.equal(result.ok, true, result.blockers.join('; ')); assert.deepEqual(result.exportTypes, ['visitor-list']); const plan = plans.buildConfirmationExportPlan(operation, {}); assert.equal(plan.target_kind, 'shared_plan'); assert.deepEqual(plan.artifacts, [{ type: 'visitor-list', path: '/System/Business/orders_Visitor.asp', params: { tid: '456' }, downloaded: false, converted: false, scheduled: false, sent: false }]); const wrongType = structuredClone(operation); wrongType.data.confirmation = { type: 'liantai-confirm' }; assert.match(plans.validateOperation(wrongType).blockers.join('\n'), /散拼母团.*整团游客信息/); const leakedChildRef = structuredClone(operation); leakedChildRef.data.existing_refs.ddid = '789'; assert.match(plans.validateOperation(leakedChildRef).blockers.join('\n'), /散拼母团.*子单.*ddid/); }); test('rejects requested confirmation formats because platform processing is separate', () => { const result = plans.validateOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001' }, confirmation: { type: 'xingyou-confirm', format: 'docx' } } }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /游客信息保留源 XLS 并由平台生成 XLSX.*format/); }); test('rejects unverified confirmation language variants', () => { const result = plans.validateOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001' }, confirmation: { type: 'xingyou-confirm', language: 'zh-CN' } } }); assert.equal(result.ok, false); assert.match(result.blockers.join('\n'), /只允许 canonical type/); }); test('dispatch gate accepts parse-state lifecycle input but strict gate waits for ERP refs', () => { const tsv = [ plans.PASSENGER_HEADERS.join('\t'), ['1', '测试游客', 'TEST YOUKE', '男', '1990-01-01', '湖南', '护照', 'P10001', '湖南', '2025-01-01', '2035-01-01', '13800000000', '普通备注'].join('\t') ].join('\n'); const parsed = { action: 'passenger_list_import', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001' }, passenger_list: { operation: 'import', row_count: 1, text: tsv } } }; const dispatch = plans.validateDispatchOperation(parsed); assert.equal(dispatch.ok, true); assert.equal(dispatch.execution, 'browser_lifecycle'); assert.equal(dispatch.requiresErpResolution, true); const strict = plans.validateOperation(parsed); assert.equal(strict.ok, false); assert.match(strict.blockers.join('\n'), /ERP 唯一解析|tid|first_import/); }); test('dispatch gate accepts business-field lookup without changing action routing', () => { const lookup = { customer: { name: '辽宁康辉', keyword: '辽宁康辉' }, departure_dates: ['2026-09-15'], product: { name: '老挝广东8D', keyword: '老挝广东8D' }, leader: { name: '张三', keyword: '张三' } }; const operations = [ { action: 'shared_child_order_create', data: { ...lookup, passenger_counts: { adult: 15, leader: 1 } } }, { action: 'order_update_shared_child', data: { ...lookup, existing_refs: { kind: 'shared_child_order' }, updates: { actions: [{ target: 'lodging_note', operation: 'append', value: '领队单住' }] } } }, { action: 'order_cancel', data: { ...lookup } }, { action: 'confirmation_export', data: { ...lookup, confirmation: { type: 'liantai-confirm' } } } ].map((operation) => ({ order_nature: 'formal', submit_mode: 'dry_run', ...operation })); for (const operation of operations) { const dispatch = plans.validateDispatchOperation(operation); assert.equal(dispatch.ok, true, `${operation.action}: ${dispatch.blockers.join('; ')}`); assert.equal(dispatch.requiresErpResolution, true); } const arrangementWithoutIdentifier = { action: 'arrangement_guide', order_nature: 'formal', submit_mode: 'dry_run', data: { ...lookup, existing_refs: { kind: 'independent_order' }, arrangement: { mode: 'create', resource: { name: '导游甲', keyword: '导游甲' } } } }; const arrangementDispatch = plans.validateDispatchOperation(arrangementWithoutIdentifier); assert.equal(arrangementDispatch.ok, false); assert.match(arrangementDispatch.blockers.join('\n'), /identifier|未声明字段/); assert.equal(operations[0].action, 'shared_child_order_create'); assert.equal(plans.validateOperation(operations[1]).ok, false); }); test('strict lifecycle gate accepts only ERP-enriched production operation', () => { const resolved = { action: 'order_update_independent', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001', tid: '101', ddid: '102', resolved: true, resolution_source: 'erp_unique_match' }, resolution: { stage: 'erp_readonly', target: 'unique', candidate_count: 1 }, updates: { actions: [{ target: 'twin_room_count', operation: 'set', value: 9 }] } } }; const strict = plans.validateOperation(resolved); assert.equal(strict.ok, true); assert.equal(strict.execution, 'browser_lifecycle'); assert.equal(strict.noErpWrite, false); const forged = structuredClone(resolved); delete forged.data.existing_refs.resolution_source; assert.equal(plans.validateOperation(forged).ok, false); }); test('execution operation rejects platform task, session, receipt and batch metadata', () => { const operation = { action: 'order_update_independent', order_nature: 'formal', submit_mode: 'dry_run', source: { task_id: 'TASK-PLATFORM-ONLY', execution_id: 'EXECUTION-PLATFORM-ONLY', instruction_id: 'INSTRUCTION-PLATFORM-ONLY', received_at: '2026-08-12T00:00:00.000Z', batch_index: 1, batch_total: 2 }, data: { existing_refs: { kind: 'independent_order', identifier: 'LW-EXAMPLE-001', tid: '101', ddid: '102', resolved: true, resolution_source: 'erp_unique_match' }, updates: { actions: [{ target: 'twin_room_count', operation: 'set', value: 9 }] } } }; assert.equal(plans.validateOperation(operation).ok, false); assert.match(validateStandardOperation(operation).join('\n'), /source 包含未声明字段.*task_id.*execution_id.*instruction_id.*received_at.*batch_index.*batch_total/); }); test('dispatch gate keeps internal ids, status defaults, and side-effect policy out of user input', () => { const valid = plans.validateDispatchOperation({ action: 'arrangement_hotel', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'LW-EXAMPLE-001' }, arrangement: { mode: 'create', resource: { name: '万象大酒店', keyword: '万象大酒店' }, room_type: '标间', start_date: '2026-09-03', end_date: '2026-09-05', room_count: 8 } } }); assert.equal(valid.ok, true); const leaked = plans.validateDispatchOperation({ action: 'arrangement_hotel', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'LW-EXAMPLE-001', tid: '101' }, arrangement: { mode: 'create', resource: { name: '万象大酒店', keyword: '万象大酒店', id: '9', resolved: true }, room_type: '标间', start_date: '2026-09-03', end_date: '2026-09-05', room_count: 8, status: '未确认', side_effect_policy: 'no_external' } } }); assert.equal(leaked.ok, false); assert.match(leaked.blockers.join('\n'), /内部引用|执行态字段/); }); test('dispatch export needs only a visible identifier and file type', () => { const dispatch = plans.validateDispatchOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'D14457' }, confirmation: { type: 'visitor-list' } } }); assert.equal(dispatch.ok, true); assert.equal(dispatch.requiresErpResolution, true); assert.deepEqual(dispatch.exportTypes, ['visitor-list']); }); test('dispatch keeps an explicit historical export fixture on its strict test-context path', () => { const dispatch = plans.validateDispatchOperation({ action: 'confirmation_export', order_nature: 'formal', submit_mode: 'dry_run', source: { test_context: { run_id: 'historical-export-replay', marker: 'TEST-202609', account: '测试ai员工账号', native_baseline_id: 'historical-baseline', allow_live_write: false } }, data: { existing_refs: { kind: 'independent_order', identifier: 'LW-260903-TEST-202609', tid: '101', ddid: '102' }, confirmation: { type: 'visitor-list' } } }); assert.equal(dispatch.ok, true, dispatch.blockers.join('; ')); assert.equal(dispatch.phase, 'dispatch'); assert.equal(dispatch.requiresErpResolution, false); }); test('strict gate accepts the ERP-enriched production lifecycle matrix without test metadata', () => { const refs = (kind, identifier) => ({ kind, identifier, tid: '101', ...(kind === 'shared_plan' ? {} : { ddid: '102' }), ...(kind === 'shared_child_order' ? { child_order_no: identifier, parent_group_no: 'LW-PARENT-001' } : {}), resolved: true, resolution_source: 'erp_unique_match' }); const resolution = { stage: 'erp_readonly', target: 'unique', candidate_count: 1 }; const named = (name, id) => ({ name, keyword: name, id, resolved: true }); const tsv = [ plans.PASSENGER_HEADERS.join('\t'), ['1', '测试游客', 'TEST YOUKE', '男', '1990-01-01', '湖南', '护照', 'P10001', '湖南', '2025-01-01', '2035-01-01', '13800000000', '普通备注'].join('\t') ].join('\n'); const operations = [ { action: 'passenger_list_import', data: { existing_refs: { ...refs('independent_order', 'LW-001'), expected_passenger_count: 1 }, passenger_list: { operation: 'first_import', row_count: 1, text: tsv }, resolution } }, { action: 'arrangement_guide', data: { existing_refs: { ...refs('independent_order', 'LW-001'), owner_account: '当前OP' }, arrangement: { mode: 'create', resource: named('测试导游', '11'), status: '未确认', side_effect_policy: 'no_external', remark: '' }, resolution } }, { action: 'arrangement_vehicle', data: { existing_refs: refs('shared_plan', 'LW-PARENT-001'), arrangement: { mode: 'create', supplier: named('测试车队', '12'), item: '商务车', quantity: 1, start_date: '2026-10-01', end_date: '2026-10-03', status: '未确认', side_effect_policy: 'no_external', remark: '' }, resolution } }, { action: 'arrangement_hotel', data: { existing_refs: refs('independent_order', 'LW-001'), arrangement: { mode: 'create', resource: named('万象大酒店', '13'), room_type: '标间', start_date: '2026-10-01', end_date: '2026-10-03', room_count: 8, status: '未安排', side_effect_policy: 'no_external', remark: '' }, resolution } }, { action: 'arrangement_transport', data: { existing_refs: refs('shared_plan', 'LW-PARENT-001'), arrangement: { mode: 'create', supplier: named('测试票务', '14'), item: '测试航段', quantity: 16, date: '2026-10-01', status: '未确认', side_effect_policy: 'no_external', remark: '' }, resolution } }, { action: 'arrangement_other', data: { existing_refs: refs('shared_plan', 'LW-PARENT-001'), arrangement: { mode: 'create', supplier: named('测试供应商', '15'), item: '备案服务', quantity: 1, date: '2026-10-01', status: '未确认', side_effect_policy: 'no_external', remark: '', filing: { number: 'A-1', date: '2026-10-01' } }, resolution } }, { action: 'order_update_independent', data: { existing_refs: refs('independent_order', 'LW-001'), updates: { actions: [{ target: 'twin_room_count', operation: 'set', value: 9 }] }, resolution } }, { action: 'order_update_shared_plan', data: { existing_refs: refs('shared_plan', 'LW-PARENT-001'), updates: { actions: [{ target: 'planned_capacity', operation: 'set', value: 40 }] }, resolution } }, { action: 'order_update_shared_child', data: { existing_refs: refs('shared_child_order', 'D10001'), updates: { actions: [{ target: 'lodging_note', operation: 'append', value: '新增住宿说明' }] }, resolution } }, { action: 'order_cancel', data: { existing_refs: refs('independent_order', 'LW-001'), transition: { mode: 'edit', target_kind: 'independent_order', from_status: '预订', to_status: '已取消' }, resolution } }, { action: 'order_restore', data: { existing_refs: refs('shared_plan', 'LW-PARENT-001'), transition: { mode: 'edit', target_kind: 'shared_plan', from_status: '已取消', to_status: '收客中' }, resolution } }, { action: 'confirmation_export', data: { existing_refs: refs('shared_child_order', 'D10001'), confirmation: { type: 'visitor-list' }, resolution } } ].map((operation) => ({ ...operation, order_nature: 'formal', submit_mode: 'dry_run' })); for (const operation of operations) { const result = plans.validateOperation(operation); assert.equal(result.ok, true, `${operation.action}: ${result.blockers.join('; ')}`); assert.deepEqual(validateStandardOperation(operation), [], `${operation.action}: execution validator mismatch`); } });