Files
LWLT-AIBOT/control-plane/test/hotel-batch.test.ts
2026-09-24 14:43:08 +08:00

94 lines
6.7 KiB
TypeScript
Raw 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.

import { readFileSync } from 'node:fs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseProgramInput } from '../src/program-parser.js';
import { classifyExecutionResult, successReceiptFromResult } from '../src/task-service.js';
import { hotelBatch, hotelBatchMatchesOperation, verifiedHotelBatchItems } from '../src/hotel-batch.js';
import { partialBatchProgressEvents } from '../src/team-progress.js';
import { arrangementEventFromSuccess } from '../src/arrangement-ledger.js';
import { buildCustomerSuccessReceipt, replyTextFromReceipt } from '../src/reply-contract.js';
const block = (day: number, group = 'LW-SAMPLE') => `团号:${group}\n入住日期:2026-09-${day}\n离店日期:2026-09-${day + 1}\n酒店搜索:示例酒店${day}TWN\n间数:1`;
const parse = (rawText: string) => parseProgramInput({ rawText, receivedAt: '2026-09-24T00:00:00Z' });
type Json = Record<string, any>;
async function fixture() {
const parsed = await parse(`安排酒店\n${block(26)}\n${block(27)}\n${block(28)}`);
assert.equal(parsed.status, 'agent_parse_passed', parsed.reply as string);
const operation = parsed.operation as Json;
const items = operation.data.arrangements.map((arrangement: Json, index: number) => ({ index, arrangement, status: 'completed',
result: { status: 'completed', write_attempted: true, no_erp_write: false, report: { status: 'lifecycle_completed',
server_response: { completed: true }, resolved_refs: { kind: 'independent_order', identifier: 'LW-SAMPLE', tid: '1' },
requery: { matched: true, subpage: { arrangement_target: { row_id: String(index + 10), quantity_value: '1' },
checks: [{ field: 'riqi0', actual: arrangement.start_date, matched: true }, { field: 'riqis0', actual: arrangement.end_date, matched: true }] } }
} } }));
return { operation, result: { status: 'completed', write_attempted: true, no_erp_write: false,
hotel_batch: { version: 1, group_number: 'LW-SAMPLE', items } } as Json };
}
test('hotel input splits five repeated group blocks and preserves multi-night stays and ordinary input', async () => {
const result = await parse(`安排酒店\n${[25,26,27,28,29].map(day => block(day)).join('\n')}`);
assert.equal(result.status, 'agent_parse_passed');
assert.equal((result.operation?.data as Json).arrangements.length, 5);
const single = await parse(`安排酒店\n${block(26).replace('2026-09-27', '2026-09-29')}`);
assert.equal((single.operation?.data as Json).arrangement.end_date, '2026-09-29');
assert.equal((single.operation?.data as Json).arrangements, undefined);
});
test('the entire batch blocks on cross-team, missing field, duplicate, mixed action, invalid date and oversized input', async () => {
for (const raw of [
`${block(26)}\n${block(27, 'OTHER')}`, `${block(26)}\n${block(27).replace('间数:1','')}`,
`${block(26)}\n${block(26)}`, `${block(26)}\n安排用车\n${block(27)}`,
`${block(26)}\n${block(27).replace('2026-09-28','2026-09-26')}`,
Array.from({ length:11 },(_, i)=>block(i + 10)).join('\n')
]) {
const result = await parse(`安排酒店\n${raw}`);
assert.notEqual(result.status, 'agent_parse_passed', raw);
assert.equal(result.operation, null);
assert.equal(result.no_erp_write, true);
}
});
test('all rows require bound, distinct write/requery evidence before whole-batch success', async () => {
const { operation, result } = await fixture();
assert.equal(classifyExecutionResult(result).status, 'completed');
assert.ok(hotelBatchMatchesOperation(operation, result));
const reply = replyTextFromReceipt(buildCustomerSuccessReceipt(successReceiptFromResult(result)!, result, operation));
assert.match(reply, /酒店批量安排成功/); assert.match(reply, /成功 3 条/); assert.match(reply, /2026\/09\/28/);
for (const mutate of [
(r: Json) => { r.hotel_batch.items[1].result.report.server_response.completed = false; },
(r: Json) => { r.hotel_batch.items[1].result.report.requery.subpage.arrangement_target.row_id = '10'; },
(r: Json) => { r.hotel_batch.items[1].result.report.requery.subpage.checks[0].actual = '2026-01-01'; }
]) { const bad = structuredClone(result); mutate(bad); assert.equal(classifyExecutionResult(bad).status, 'reconciliation_pending'); }
const bad = structuredClone(result); bad.hotel_batch.items[1].arrangement.resource.keyword = 'different';
assert.equal(hotelBatchMatchesOperation(operation, bad), false);
});
test('partial stop retains distinct successful ledger events and uncertainty never counts as success', async () => {
const { operation, result } = await fixture();
result.status = 'blocked';
const last = result.hotel_batch.items[2];
last.status = 'failed'; last.result = { status: 'blocked', no_erp_write: true, write_attempted: false, message: '未匹配酒店' };
assert.equal(classifyExecutionResult(result).status, 'blocked');
assert.equal(successReceiptFromResult(result), null);
const input = { taskId:'TASK', executionId:'EXEC', operation, result, receipt:null, effectiveAt:'2026-09-24T00:00:00Z', recordedAt:'2026-09-24T00:00:00Z' };
const events = partialBatchProgressEvents(input);
assert.equal(events.length, 2); assert.notEqual(events[0].execution_id, events[1].execution_id);
const arrangements = verifiedHotelBatchItems(operation, result).map(item => arrangementEventFromSuccess({ ...input,
executionId:`EXEC:hotel:${item.index + 1}`, operation:{ action:'arrangement_hotel', data:{ existing_refs:operation.data.existing_refs, arrangement:item.arrangement } },
result:item.result, receipt:successReceiptFromResult(item.result) }));
assert.ok(arrangements.every(Boolean)); assert.notEqual(arrangements[0]?.record_key, arrangements[1]?.record_key);
last.status = 'uncertain'; last.result = { status:'execution_uncertain', write_attempted:true, no_erp_write:false };
result.status = 'execution_uncertain';
assert.equal(classifyExecutionResult(result).status, 'reconciliation_pending');
assert.equal(partialBatchProgressEvents(input).length, 2);
assert.match(hotelBatch.resultText(result.hotel_batch), /待核验 1 条/);
});
test('published operator batch example parses to three complete ordered stays', async () => {
const catalog = readFileSync('agent设计规范/templates/business-input-templates.md','utf8');
const section = catalog.split('#### 同团批量安排示例')[1].split('### 11.')[0];
const rawText = section.match(/```text\n([\s\S]*?)```/)![1];
const parsed = await parse(rawText);
assert.equal(parsed.status, 'agent_parse_passed');
const arrangements = (parsed.operation!.data as Json).arrangements;
assert.equal(arrangements.length,3);
assert.equal(arrangements[2].start_date,'2026-09-28');
assert.equal(arrangements[2].end_date,'2026-09-30');
});