Files
LWLT-AI/tools/operation-plans.test.mjs
2026-07-13 19:57:46 +08:00

237 lines
8.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
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: 'test',
submit_mode: 'dry_run',
data: {
test_marker: 'PLAN-TEST',
product: { name: '遇见老挝' },
departure_dates: ['2026-08-11'],
passenger_counts: { adult: 2, expected_total: 2 },
op_user: { name: '测试OP' },
sales_user: { name: '测试销售' },
},
};
test('team-single remains the only live browser execution 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('team-batch absorbs the handoff fallback rule without enabling native batch submit', () => {
const operation = {
...baseCreate,
action: 'team_order_batch_create',
data: {
...baseCreate.data,
departure_dates: ['2026-08-11', '2026-08-18'],
},
};
const result = plans.validateOperation(operation);
assert.equal(result.ok, true);
assert.equal(result.execution, 'browser_team_single_fallback');
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-8-11', '2026-8-18']);
assert.deepEqual(expanded.map((item) => item.data.test_marker), [
'PLAN-TEST-D20260811',
'PLAN-TEST-D20260818',
]);
});
test('order updates require an identifier and only run a no-write browser preview', () => {
const result = plans.validateOperation({
action: 'order_update',
identifier: 'LW-270520A-A',
data: {
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('parent confirmation export is blocked before any browser operation', () => {
const result = plans.validateOperation({
action: 'confirmation_export',
route: 'split_parent',
identifier: 'PARENT-EXAMPLE',
data: { existing_refs: { parent_group_no: 'PARENT-EXAMPLE' } },
});
assert.equal(result.ok, false);
assert.match(result.blockers.join('\n'), /母团/);
assert.equal(result.execution, 'blocked');
});
test('confirmation export normalizes supported aliases and blocks unknown artifacts', () => {
const valid = plans.validateOperation({
action: 'confirmation_export',
identifier: 'LW-270520A-A',
exportTypes: ['confirmation', 'job'],
data: { existing_refs: { identifier: 'LW-270520A-A' } },
});
assert.equal(valid.ok, true);
assert.deepEqual(valid.exportTypes, ['xingyou-confirm', 'job-order']);
const invalid = plans.validateOperation({
action: 'confirmation_export',
identifier: 'LW-270520A-A',
exportTypes: ['visitor-detail'],
data: { existing_refs: { identifier: 'LW-270520A-A' } },
});
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('normalizes handoff create tasks without inventing missing ERP values', () => {
const operation = plans.normalizeOperation({
operation: 'create_order',
route: 'team_single',
fields: {
productName: '遇见老挝',
departureDate: '2026-08-11',
pax: 4,
op: '测试OP',
salesperson: '测试销售',
},
originalText: '交接包格式任务',
});
assert.equal(operation.action, 'team_order_create');
assert.equal(operation.data.product.name, '遇见老挝');
assert.equal(operation.data.passenger_counts.adult, 4);
assert.equal(operation.data.customer.name, '');
assert.equal(operation.data.test_marker, '');
});
test('routes a test split-parent task into the guarded live browser adapter', () => {
const operation = plans.normalizeOperation({
operation: 'create_order',
route: 'split_parent',
fields: {
productName: '示例线路',
departureDates: ['2026-08-15'],
cycle: 'weekly',
plannedPax: 20,
groupsPerDate: 1,
bookingCustomer: '测试渠道',
followOp: '测试OP',
salesperson: '测试销售',
orderNature: 'test',
testMarker: 'SPLIT-PARENT-TEST',
},
});
const result = plans.validateOperation(operation);
assert.equal(operation.action, 'shared_plan_create');
assert.equal(result.ok, true);
assert.equal(result.execution, 'browser_live_split_parent');
assert.equal(result.definition.liveSubmit, 'test-only');
assert.equal(operation.data.planned_capacity, 20);
assert.equal(operation.data.op_user.name, '测试OP');
});
test('absorbs split-child parent reference without fabricating a product or parent id', () => {
const operation = plans.normalizeOperation({
operation: 'create_order',
route: 'split_child',
fields: {
parentGroupNo: 'PARENT-EXAMPLE',
departureDate: '2026-08-15',
bookingCustomer: '测试渠道',
pax: 8,
prices: { adult: 100 },
op: '测试OP',
salesperson: '测试销售',
orderNature: 'test',
testMarker: 'SPLIT-CHILD-TEST',
},
});
const result = plans.validateOperation(operation);
assert.equal(operation.action, 'shared_child_order_create');
assert.equal(result.ok, true);
assert.equal(operation.data.parent_group_no, 'PARENT-EXAMPLE');
assert.equal(operation.data.product.name, '');
assert.equal(result.execution, 'browser_live_split_child');
});
test('routes a traveler-only handoff update into the traveler import business action', () => {
const operation = plans.normalizeOperation({
operation: 'update_order',
identifier: 'LW-270520A-A',
updatePlan: {
actions: [{ target: 'travelerList', operation: 'traveler_upsert', value: { mode: 'append_and_verify' } }],
},
attachments: ['inputs/traveler-list-example.xlsx'],
});
const result = plans.validateOperation(operation);
assert.equal(operation.action, 'passenger_list_import');
assert.equal(result.ok, true);
assert.equal(result.execution, 'browser_dry_run');
assert.equal(result.definition.dryRunAdapter, 'inspect_existing_order_traveler');
});
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',
identifier: 'LW-270520A-A',
exportTypes: ['confirmation', 'liantai', 'job'],
data: { existing_refs: { identifier: 'LW-270520A-A' } },
}, { ddid: '123', tid: '456' });
assert.equal(plan.export_only, true);
assert.equal(plan.never_resave, true);
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);
});