迭代批量安排酒店
This commit is contained in:
1 parent
4cfbefffe8
commit
c2c71d35af
48 files changed
+5670
-57
No files matched your search
@@ -0,0 +1,93 @@
|
||||
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');
|
||||
});
|
||||
@@ -189,7 +189,7 @@ test('versioned sanitized gold corpus replays deterministically', async () => {
|
||||
const corpus = JSON.parse(await readFile(
|
||||
new URL('../../samples/business-parser-gold-corpus.json', import.meta.url), 'utf8'
|
||||
));
|
||||
assert.equal(corpus.version, 'ltjt-program-parser-v1.0.14');
|
||||
assert.equal(corpus.version, 'ltjt-program-parser-v1.0.15');
|
||||
assert.ok(corpus.cases.length >= 28);
|
||||
for (const candidate of corpus.cases) {
|
||||
const parse = () => parseProgramInput({
|
||||
@@ -863,3 +863,22 @@ test('mode orchestration keeps AI authority in Shadow and enforces the Auto fall
|
||||
assert.equal(affinity.decision.authoritativeEngine, 'ai');
|
||||
assert.equal(aiCalls, 3);
|
||||
});
|
||||
|
||||
|
||||
test('repeated hotel blocks always use their deterministic batch contract even with AI affinity', async () => {
|
||||
let aiCalls = 0;
|
||||
const orchestrator = new ParserOrchestrator({ async parse() { aiCalls++; return {}; } });
|
||||
const rawText = '安排酒店\n' + [26,27].map(day => `团号:LW-SAMPLE\n入住日期:2026-09-${day}\n离店日期:2026-09-${day+1}\n酒店搜索:示例酒店TWN\n间数:1`).join('\n');
|
||||
for (const mode of ['ai','shadow','auto','program'] as const) {
|
||||
const claim = orchestrationClaim(mode, rawText, 'ai');
|
||||
claim.task.parser.route_id = 'arrangement_hotel_create';
|
||||
const parsed = await orchestrator.parse(claim);
|
||||
assert.equal(parsed.decision.authoritativeEngine, 'program');
|
||||
assert.equal((parsed.result as any).operation.data.arrangements.length, 2);
|
||||
claim.messageText = '间数:2';
|
||||
claim.history = [{ turnNo: 1, rawText }];
|
||||
const incomplete = await orchestrator.parse(claim);
|
||||
assert.equal((incomplete.result as any).error_code, 'program_hotel_batch_invalid');
|
||||
}
|
||||
assert.equal(aiCalls, 0);
|
||||
});
|
||||
Reference in new issue
Block a user