Files
LWLT-AIBOT/tools/operation-plans.test.mjs

938 lines
36 KiB
JavaScript

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`);
}
});