Files
LWLT-AI/archive/handoff/2026-07-12/legacy-erp-handoff/tools/tests/erp_task_dispatcher.test.js
2026-07-13 19:57:46 +08:00

1004 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const dispatcher = require('../erp_task_dispatcher');
function tempAuditDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'erp-dispatch-audit-'));
}
function readyTeamBatchEnvelope() {
return {
status: 'ready',
route: 'team_batch',
task: {
taskId: 'task-team-batch-1',
route: 'team_batch',
orderNature: 'test',
fields: {
bookingCustomer: 'LW customer',
productName: 'Laos 6D5N',
departureDates: ['2026-07-13', '2026-07-14', '2026-07-15'],
defaultPax: { adult: 2, childBed: 1, childNoBed: 1, infant: 0, leader: 0 },
defaultRooms: { SGL: 0, TWN: 0, TRP: 0, DBL: 2, HNM: 0, TL: 0 },
defaultPrices: { adult: 520, childBed: 220, childNoBed: 120, infant: 0, leader: 0 },
op: 'test-op',
sales: 'sales-a',
},
erpIdentifiers: {},
},
};
}
function readyTeamSingleEnvelope() {
return {
status: 'ready',
route: 'team_single',
task: {
taskId: 'task-team-single-1',
route: 'team_single',
orderNature: 'test',
fields: {
productName: 'Laos',
departureDate: '2026-07-12',
pax: { adult: 2, childBed: 1, childNoBed: 1, infant: 0, leader: 0 },
rooms: { SGL: 0, TWN: 0, TRP: 0, DBL: 2, HNM: 0, TL: 0 },
prices: { adult: 500, childBed: 200, childNoBed: 100, infant: 0, leader: 0 },
op: 'test-op',
sales: 'sales-a',
},
erpIdentifiers: {},
},
};
}
function readySplitParentEnvelope() {
return {
status: 'ready',
route: 'split_parent',
task: {
taskId: 'task-split-parent-1',
route: 'split_parent',
orderNature: 'test',
fields: {
productRoute: 'Laos route Guangdong 8D7N',
dateRange: { start: '2026-07-01', end: '2026-07-31' },
cycle: 'weekly-saturday',
departureDates: ['2026-07-04', '2026-07-11'],
plannedGuests: 10,
parentPlansPerDate: 1,
followOp: 'test-op',
},
erpIdentifiers: {},
},
};
}
function readySplitChildEnvelope() {
return {
status: 'ready',
route: 'split_child',
task: {
taskId: 'task-split-child-1',
route: 'split_child',
orderNature: 'test',
fields: {
parentGroupNo: 'LW-260701A-F',
departureDate: '2026-07-01',
channelCustomer: 'LW customer',
pax: { adult: 5, childBed: 0, childNoBed: 0, infant: 0, leader: 0 },
prices: { adult: 500, childBed: 0, childNoBed: 0, infant: 0, leader: 0 },
op: 'test-op',
salesperson: 'sales-a',
},
erpIdentifiers: {},
},
};
}
function withTravelerList(envelope, totalCount = 1) {
const cloned = JSON.parse(JSON.stringify(envelope));
cloned.task.fields.travelerList = {
totalCount,
fieldStatus: {
chineseName: true,
englishName: true,
gender: true,
birthDate: true,
birthPlace: true,
passportNo: true,
issueDate: true,
expiryDate: true,
issuePlace: true,
remark: true,
},
rows: Array.from({ length: totalCount }, () => ({
chineseName: 'TEST',
englishName: 'TEST GUEST',
gender: 'X',
})),
};
return cloned;
}
test('blocks adapter results that are not ready', async () => {
let called = false;
const result = await dispatcher.dispatchTask(
{
status: 'needs_clarification',
route: 'team_single',
customerMessage: 'missing sales',
task: null,
},
{
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
team_single: async () => {
called = true;
return { status: 'saved' };
},
},
}
);
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'adapter_not_ready');
assert.equal(called, false);
assert.match(result.customerMessage, /missing sales/);
assert.ok(fs.existsSync(result.auditPath));
});
test('blocks invalid ready tasks before invoking a route handler', async () => {
let called = false;
const result = await dispatcher.dispatchTask(
{
status: 'ready',
operation: 'create_order',
task: {
operation: 'create_order',
fields: { productName: '遇见老挝' },
},
},
{
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
team_single: async () => {
called = true;
return { status: 'completed' };
},
},
}
);
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'invalid_task');
assert.equal(called, false);
assert.ok(Array.isArray(result.violations));
});
test('dry-run accepts ready tasks without calling route handlers', async () => {
let called = false;
const result = await dispatcher.dispatchTask(readyTeamBatchEnvelope(), {
mode: 'dry-run',
auditDir: tempAuditDir(),
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'dry_run');
assert.equal(result.route, 'team_batch');
assert.equal(called, false);
assert.match(result.customerMessage, /ready for ERP execution/i);
});
test('formatCompletionMessage includes duplicate warning messages', () => {
const message = dispatcher.formatCompletionMessage(readyTeamSingleEnvelope().task, {
identifiers: { groupNo: 'LLW-260712A-B' },
warnings: [
{
reason: 'same_date_product_order_exists',
message: 'ERP already has LLW-260712A-A; continued with a new order.',
},
],
});
assert.match(message, /ERP already has LLW-260712A-A/);
assert.match(message, /LLW-260712A-B/);
});
test('execute mode requires the real-submit safety switch', async () => {
let called = false;
const result = await dispatcher.dispatchTask(readyTeamBatchEnvelope(), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: false } },
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'real_submit_disabled');
assert.equal(called, false);
assert.match(result.customerMessage, /real ERP submit is disabled/i);
});
test('execute mode blocks when real-submit whitelist is enabled without sender metadata', async () => {
let called = false;
const result = await dispatcher.dispatchTask(readyTeamBatchEnvelope(), {
mode: 'execute',
auditDir: tempAuditDir(),
config: {
safety: {
allowRealSubmit: true,
realSubmitWhitelist: {
enabled: true,
allowedSenderIds: ['wxid-authorized'],
},
},
},
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'real_submit_sender_not_authorized');
assert.equal(called, false);
assert.match(result.customerMessage, /白名单/);
});
test('execute mode blocks non-whitelisted senders', async () => {
let called = false;
const result = await dispatcher.dispatchTask(readyTeamBatchEnvelope(), {
mode: 'execute',
auditDir: tempAuditDir(),
config: {
safety: {
allowRealSubmit: true,
realSubmitWhitelist: {
enabled: true,
allowedSenderIds: ['wxid-authorized'],
},
},
},
source: {
senderId: 'wxid-someone-else',
},
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'real_submit_sender_not_authorized');
assert.equal(called, false);
});
test('execute mode allows whitelisted senders', async () => {
let called = false;
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamBatchEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: {
safety: {
allowRealSubmit: true,
realSubmitWhitelist: {
enabled: true,
allowedSenderIds: ['wxid-authorized'],
},
},
},
source: {
senderId: 'WXID-AUTHORIZED',
},
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'saved');
assert.equal(called, true);
});
test('execute mode can use configured authorization code fallback', async () => {
let called = false;
const envelope = withTravelerList(readyTeamBatchEnvelope());
envelope.task.originalText = '备注:确认真实保存 ERP。真实保存授权码REAL-20260620';
envelope.task.fields.remark = '确认真实保存 ERP。真实保存授权码REAL-20260620';
const result = await dispatcher.dispatchTask(envelope, {
mode: 'execute',
auditDir: tempAuditDir(),
config: {
safety: {
allowRealSubmit: true,
realSubmitWhitelist: {
enabled: true,
allowedSenderIds: ['wxid-authorized'],
allowAuthorizationCodeFallback: true,
authorizationCodes: ['real-20260620'],
},
},
},
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'saved');
assert.equal(called, true);
});
test('execute mode blocks orders whose text explicitly forbids real ERP save', async () => {
let called = false;
const envelope = readyTeamBatchEnvelope();
envelope.task.originalText = '备注:爱马仕 dry-run 测试,不要真实保存 ERP。';
envelope.task.fields.remark = '爱马仕 dry-run 测试,不要真实保存 ERP。';
const result = await dispatcher.dispatchTask(envelope, {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'order_text_forbids_real_submit');
assert.equal(called, false);
assert.match(result.customerMessage, /forbids real ERP submit/i);
});
test('execute mode blocks a rewritten same order after a recent no-save block', async () => {
let called = false;
const auditDir = tempAuditDir();
const prior = readyTeamBatchEnvelope();
prior.task.originalText = 'remark: dry-run only, do not save ERP';
prior.task.fields.remark = 'dry-run only, do not save ERP';
fs.writeFileSync(
path.join(auditDir, 'dispatch-20260619081202-team_batch.json'),
JSON.stringify({
status: 'blocked',
reason: 'order_text_forbids_real_submit',
mode: 'execute',
route: 'team_batch',
task: prior.task,
}),
'utf8'
);
const rewritten = readyTeamBatchEnvelope();
rewritten.task.originalText = 'remark: execute test order';
rewritten.task.fields.remark = 'execute test order';
const result = await dispatcher.dispatchTask(rewritten, {
mode: 'execute',
auditDir,
config: { safety: { allowRealSubmit: true } },
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'recent_no_save_block_for_same_order');
assert.equal(called, false);
assert.match(result.customerMessage, /recent no-save block/i);
});
test('execute mode allows a rewritten same order after explicit real-save confirmation', async () => {
let called = false;
const auditDir = tempAuditDir();
const prior = readyTeamBatchEnvelope();
prior.task.originalText = 'remark: dry-run only, do not save ERP';
prior.task.fields.remark = 'dry-run only, do not save ERP';
fs.writeFileSync(
path.join(auditDir, 'dispatch-20260619081202-team_batch.json'),
JSON.stringify({
status: 'blocked',
reason: 'order_text_forbids_real_submit',
mode: 'execute',
route: 'team_batch',
task: prior.task,
}),
'utf8'
);
const confirmed = withTravelerList(readyTeamBatchEnvelope());
confirmed.task.originalText = 'remark: execute test order. 确认真实保存 ERP';
confirmed.task.fields.remark = 'execute test order. 确认真实保存 ERP';
const result = await dispatcher.dispatchTask(confirmed, {
mode: 'execute',
auditDir,
config: { safety: { allowRealSubmit: true } },
handlers: {
team_batch: async () => {
called = true;
return { status: 'saved' };
},
},
});
assert.equal(result.status, 'saved');
assert.equal(called, true);
});
test('execute mode blocks a duplicate completed update_order task', async () => {
let called = false;
const auditDir = tempAuditDir();
const updatePlan = {
status: 'ready',
missingFields: [],
actions: [
{ target: 'pax.adult', operation: 'delta', value: 1, source: 'adult +1' },
{ target: 'rooms.DBL', operation: 'delta', value: 1, source: 'DBL +1' },
],
};
const completedTask = {
operation: 'update_order',
identifier: 'LW-260813A-B',
updateText: 'adult +1\nDBL +1',
updatePlan,
createdAt: new Date().toISOString(),
};
fs.writeFileSync(
path.join(auditDir, 'dispatch-20260623160941-completed.json'),
JSON.stringify({
status: 'completed',
mode: 'execute',
operation: 'update_order',
task: completedTask,
}),
'utf8'
);
const result = await dispatcher.dispatchTask({
status: 'ready',
operation: 'update_order',
task: {
...completedTask,
createdAt: new Date().toISOString(),
},
}, {
mode: 'execute',
auditDir,
config: { safety: { allowRealSubmit: true } },
handlers: {
update_order: async () => {
called = true;
return { status: 'completed' };
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'recent_duplicate_update_order');
assert.equal(called, false);
assert.match(result.customerMessage, /相同修改内容/);
assert.doesNotMatch(result.customerMessage, /same changes/i);
});
test('execute mode allows create-order routes to run without traveler lists', async () => {
let called = false;
const result = await dispatcher.dispatchTask(readyTeamSingleEnvelope(), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
team_single: async () => {
called = true;
return { status: 'completed', identifiers: { groupNo: 'LW-260710A-A' } };
},
},
});
assert.equal(result.status, 'completed');
assert.equal(result.route, 'team_single');
assert.equal(called, true);
assert.equal(result.identifiers.groupNo, 'LW-260710A-A');
});
test('execute mode does not require traveler lists for split parent plan creation', async () => {
let called = false;
const result = await dispatcher.dispatchTask(readySplitParentEnvelope(), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
split_parent: async () => {
called = true;
return { status: 'completed', identifiers: { groupNo: 'LW-260704A-A' } };
},
},
});
assert.equal(result.status, 'completed');
assert.equal(called, true);
});
test('execute mode calls the injected route handler when safety is enabled', async () => {
let receivedTask = null;
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamBatchEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
team_batch: async (task) => {
receivedTask = task;
return {
status: 'completed',
identifiers: {
dateToGroupNo: {
'2026-07-13': 'LW-260713A-A',
'2026-07-14': 'LW-260714A-A',
'2026-07-15': 'LW-260715A-A',
},
},
artifacts: [
{ type: 'pdf', path: 'outputs/LW-260713A-A-confirm.pdf' },
],
};
},
},
});
assert.equal(result.status, 'completed');
assert.equal(receivedTask.route, 'team_batch');
assert.deepEqual(result.identifiers.dateToGroupNo['2026-07-14'], 'LW-260714A-A');
assert.match(result.customerMessage, /2026-07-13: LW-260713A-A/);
assert.match(result.customerMessage, /PDF/);
});
test('execute mode wraps live-session ERP work in a file lock', async () => {
const auditDir = tempAuditDir();
const lockCalls = [];
let handlerCalled = false;
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamBatchEnvelope()), {
mode: 'execute',
auditDir,
config: {
safety: { allowRealSubmit: true },
paths: { workRoot: auditDir },
browser: {
liveSession: {
enabled: true,
lockTimeoutMs: 12345,
lockPollMs: 25,
},
},
},
taskLock: {
withFileLock: async (lockDir, fn, options) => {
lockCalls.push({ lockDir, options });
return fn();
},
},
handlers: {
team_batch: async () => {
handlerCalled = true;
return { status: 'completed', identifiers: { dateToGroupNo: { '2026-07-13': 'LW-260713A-A' } } };
},
},
});
assert.equal(result.status, 'completed');
assert.equal(handlerCalled, true);
assert.equal(lockCalls.length, 1);
assert.match(lockCalls[0].lockDir, /erp-live-session\.lock$/);
assert.deepEqual(lockCalls[0].options, { timeoutMs: 12345, pollMs: 25 });
});
test('summarizeCliResult keeps identifiers and traveler import counts without PII', () => {
const fullResult = {
status: 'completed',
mode: 'execute',
operation: 'create_order',
route: 'team_single',
customerMessage: 'ERP execution completed.',
identifiers: { groupNo: 'LLW-271116A-A' },
auditPath: 'runtime/audit/dispatch.json',
task: {
fields: {
travelerList: {
rows: [{ chineseName: '张三', passportNo: 'E12345678' }],
},
},
},
rawExecutionResult: {
stageResults: {
submit: {
rowText: 'contains 张三 and E12345678',
state: {
travelerImport: {
attempted: true,
imported: true,
method: 'erp_textarea_paste',
expectedRows: 17,
beforeRows: 17,
beforeFilledRows: 0,
afterRows: 17,
afterFilledRows: 17,
reason: '',
dialogFrame: { frame: { secret: 'playwright-frame' } },
},
},
},
},
},
registryRecords: [{
operation: 'create_order',
route: 'team_single',
identifier: 'LLW-271116A-A',
groupNo: 'LLW-271116A-A',
departureDate: '2027-11-16',
ddid: '14151',
customer: 'PII customer not needed in CLI summary',
}],
};
const summary = dispatcher.summarizeCliResult(fullResult);
const text = JSON.stringify(summary);
assert.equal(summary.status, 'completed');
assert.equal(summary.identifiers.groupNo, 'LLW-271116A-A');
assert.equal(summary.travelerImports[0].afterFilledRows, 17);
assert.equal(summary.registryRecords[0].ddid, '14151');
assert.doesNotMatch(text, /张三|E12345678|travelerList|rawExecutionResult|playwright-frame|PII customer/);
});
test('dispatcher audit redacts traveler list and supplemental traveler PII', async () => {
const auditDir = tempAuditDir();
const envelope = readyTeamSingleEnvelope();
envelope.task.fields.travelerList = {
totalCount: 1,
rows: [{
chineseName: '张三',
englishName: 'ZHANG SAN',
passportNo: 'E12345678',
idNo: '440100199001010011',
birthDate: '1990-01-01',
raw: '张三 E12345678',
}],
};
envelope.task.fields.supplemental = {
travelers: [{
name: '张三',
passportNo: 'E12345678',
idNo: '440100199001010011',
birthDate: '1990-01-01',
raw: '张三 E12345678',
}],
};
const result = await dispatcher.dispatchTask(envelope, {
mode: 'dry-run',
auditDir,
config: { safety: { allowRealSubmit: true } },
});
const audit = JSON.parse(fs.readFileSync(result.auditPath, 'utf8'));
const text = JSON.stringify(audit);
assert.equal(audit.task.fields.travelerList.rows[0].chineseName, '[redacted]');
assert.equal(audit.task.fields.travelerList.rows[0].passportNo, '[redacted]');
assert.equal(audit.task.fields.supplemental.travelers[0].name, '[redacted]');
assert.equal(audit.task.fields.supplemental.travelers[0].passportNo, '[redacted]');
assert.doesNotMatch(text, /张三|E12345678|440100199001010011|1990-01-01/);
});
test('dispatcher keeps completed audit redacted after appending registry records', async () => {
const auditDir = tempAuditDir();
const registryPath = path.join(auditDir, 'orders.jsonl');
const envelope = readyTeamSingleEnvelope();
envelope.task.fields.travelerList = {
totalCount: 1,
rows: [{
chineseName: '寮犱笁',
englishName: 'ZHANG SAN',
passportNo: 'E12345678',
idNo: '440100199001010011',
birthDate: '1990-01-01',
raw: '寮犱笁 E12345678',
}],
};
envelope.task.fields.supplemental = {
travelerList: envelope.task.fields.travelerList,
travelers: [{
name: '寮犱笁',
passportNo: 'E12345678',
idNo: '440100199001010011',
birthDate: '1990-01-01',
raw: '寮犳笁 E12345678',
}],
};
const result = await dispatcher.dispatchTask(envelope, {
mode: 'execute',
auditDir,
registryPath,
config: {
safety: { allowRealSubmit: true },
executors: { teamSingle: { enabled: true } },
},
handlers: {
team_single: async () => ({
status: 'completed',
identifiers: {
groupNo: 'LLW-260712A-A',
ddid: '14151',
},
stageResults: {
submit: {
travelerImport: {
imported: true,
method: 'erp_textarea_paste',
expectedRows: 1,
afterFilledRows: 1,
},
},
},
}),
},
});
const audit = JSON.parse(fs.readFileSync(result.auditPath, 'utf8'));
const text = JSON.stringify(audit);
assert.equal(audit.registryRecords.length, 1);
assert.equal(audit.task.fields.travelerList.rows[0].chineseName, '[redacted]');
assert.equal(audit.task.fields.supplemental.travelers[0].passportNo, '[redacted]');
assert.doesNotMatch(text, /寮犱笁|E12345678|440100199001010011|1990-01-01/);
});
test('execute mode reports missing handlers without touching ERP', async () => {
const envelope = readyTeamBatchEnvelope();
envelope.task.route = 'not_a_route';
const result = await dispatcher.dispatchTask(envelope, {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'invalid_task');
assert.match(result.violations.join('\n'), /route is required/i);
});
test('dry-run supports export_confirmation operation tasks without a route', async () => {
const result = await dispatcher.dispatchTask({
status: 'ready',
operation: 'export_confirmation',
task: {
operation: 'export_confirmation',
identifier: 'D14079',
exportTypes: ['confirmation'],
},
}, {
mode: 'dry-run',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
});
assert.equal(result.status, 'dry_run');
assert.equal(result.operation, 'export_confirmation');
assert.equal(result.plannedExecutor, 'export_confirmation');
});
test('execute mode routes export_confirmation to operation handler', async () => {
let receivedTask = null;
const result = await dispatcher.dispatchTask({
status: 'ready',
operation: 'export_confirmation',
task: {
operation: 'export_confirmation',
identifier: 'D14079',
exportTypes: ['confirmation'],
originalText: '操作类型:导出确认件\n订单编号D14079',
},
}, {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
export_confirmation: async (task) => {
receivedTask = task;
return {
status: 'completed',
identifiers: { childOrderNo: task.identifier },
artifacts: [{ type: 'pdf', path: 'outputs/D14079-confirm.pdf' }],
customerMessage: '确认件已导出。',
};
},
},
});
assert.equal(result.status, 'completed');
assert.equal(result.operation, 'export_confirmation');
assert.equal(receivedTask.identifier, 'D14079');
assert.match(result.customerMessage, /确认件已导出/);
});
test('default export_confirmation handler is connected before route executors', async () => {
const registryPath = path.join(tempAuditDir(), 'orders.jsonl');
const result = await dispatcher.dispatchTask({
status: 'ready',
operation: 'export_confirmation',
task: {
operation: 'export_confirmation',
identifier: 'D14079',
originalText: '操作类型:导出确认件\n订单编号D14079',
},
}, {
mode: 'execute',
auditDir: tempAuditDir(),
config: {
safety: { allowRealSubmit: true },
paths: { orderRegistry: registryPath },
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'order_record_not_found');
});
test('completed create_order writes registry records when registryPath is configured', async () => {
const registryPath = path.join(tempAuditDir(), 'orders.jsonl');
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamBatchEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: {
safety: { allowRealSubmit: true },
paths: { orderRegistry: registryPath },
},
handlers: {
team_batch: async () => ({
status: 'completed',
identifiers: {
dateToGroupNo: {
'2026-07-13': 'LW-260713A-A',
},
},
customerMessage: '已创建。',
}),
},
});
assert.equal(result.status, 'completed');
assert.equal(result.registryRecords.length, 1);
assert.equal(result.registryRecords[0].identifier, 'LW-260713A-A');
assert.equal(fs.existsSync(registryPath), true);
});
test('dispatcher_error preserves executor error details for audit diagnosis', async () => {
const error = new Error('submit was not confirmed');
error.code = 'erp_submit_unconfirmed';
error.details = {
ok: false,
source: 'team_batch.updateOrder:LW-260813A-B',
bodyPreview: 'modify page without success text',
};
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamBatchEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
handlers: {
team_batch: async () => {
throw error;
},
},
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'dispatcher_error');
assert.notEqual(result.customerMessage, error.message);
assert.match(result.customerMessage, /ERP/);
assert.doesNotMatch(result.customerMessage, /submit was not confirmed|team_batch\.updateOrder|bodyPreview/);
assert.equal(result.error.code, 'erp_submit_unconfirmed');
assert.deepEqual(result.error.details, error.details);
});
test('loadInputEnvelope routes raw WeChat messages through the operation adapter', async () => {
const dir = tempAuditDir();
const messagePath = path.join(dir, 'message.txt');
fs.writeFileSync(messagePath, '操作类型:导出确认件\n订单编号D14079', 'utf8');
const envelope = await dispatcher.loadInputEnvelope({
order: messagePath,
auditDir: dir,
});
assert.equal(envelope.status, 'ready');
assert.equal(envelope.operation, 'export_confirmation');
assert.equal(envelope.task.identifier, 'D14079');
});
test('default team_batch handler blocks until ERP operations are connected', async () => {
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamBatchEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'team_batch_operations_missing');
assert.match(result.customerMessage, /team_batch ERP operations are not connected/);
});
test('default team_single handler blocks until ERP operations are connected', async () => {
const result = await dispatcher.dispatchTask(withTravelerList(readyTeamSingleEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'team_single_operations_missing');
assert.match(result.customerMessage, /team_single ERP operations are not connected/);
});
test('default split_parent handler blocks until ERP operations are connected', async () => {
const result = await dispatcher.dispatchTask(readySplitParentEnvelope(), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'split_parent_operations_missing');
assert.match(result.customerMessage, /split_parent ERP operations are not connected/);
});
test('default split_child handler blocks until ERP operations are connected', async () => {
const result = await dispatcher.dispatchTask(withTravelerList(readySplitChildEnvelope()), {
mode: 'execute',
auditDir: tempAuditDir(),
config: { safety: { allowRealSubmit: true } },
});
assert.equal(result.status, 'blocked');
assert.equal(result.reason, 'split_child_operations_missing');
assert.match(result.customerMessage, /split_child ERP operations are not connected/);
});