94 lines
6.7 KiB
TypeScript
94 lines
6.7 KiB
TypeScript
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');
|
||
});
|