Files
LWLT-AIBOT/control-plane/test/passenger-roster-intake.test.ts
2026-08-31 16:04:26 +08:00

66 lines
3.3 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { isInboundAgentBusTask, parseAgentBusFrame } from '../src/agentbus.js';
import {
initialTaskIntakeState,
isPassengerRosterRouteId
} from '../src/task-service.js';
test('only roster routes pause at intake until a workbook arrives', () => {
assert.equal(isPassengerRosterRouteId('passenger_list_import_independent'), true);
assert.equal(isPassengerRosterRouteId('passenger_list_import_shared_child'), true);
assert.equal(isPassengerRosterRouteId('order_create_independent'), false);
assert.deepEqual(initialTaskIntakeState('passenger_list_import_independent'), {
status: 'awaiting_attachment',
stage: 'intake',
message: '已识别名单业务,等待 .xls 或 .xlsx 名单附件;附件到齐前不会开始解析或执行。'
});
assert.equal(initialTaskIntakeState('team_order_create').status, 'parse_queued');
assert.equal(initialTaskIntakeState(null).status, 'parse_queued');
});
test('attachment-only AgentBus frames are eligible inbound tasks', () => {
const frame = parseAgentBusFrame(JSON.stringify({
id: 'frame-roster-1',
type: 'event',
from: 'wechat:user',
payload: {
attachments: [{
name: 'roster.xlsx',
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
size: 1234,
url: 'https://files.example.com/roster.xlsx'
}]
}
}));
assert.ok(frame);
assert.equal(isInboundAgentBusTask(frame), true);
});
test('roster attachment source contract gates claims and scrubs normalized plaintext', async () => {
const source = await readFile(new URL('../src/task-service.ts', import.meta.url), 'utf8');
assert.match(source, /status = 'awaiting_attachment'/);
assert.match(source, /input_attachment\.status = 'normalized'/);
assert.match(source, /名单内容:\\n\$\{canonicalTsv\}/);
assert.match(source, /business_route_id NOT IN \([\s\S]*passenger_list_import_independent[\s\S]*passenger_list_import_shared_child/);
assert.match(source, /SET status = 'superseded', normalized_text_ciphertext = NULL/);
assert.doesNotMatch(source, /raw_(?:file|workbook)_ciphertext|content_base64[^\n]*task_input_attachments/);
});
test('a newly delivered message revalidates previously rejected attachment bytes', async () => {
const source = await readFile(new URL('../src/task-service.ts', import.meta.url), 'utf8');
assert.match(source, /const shouldValidateAttachment = !prior\.rowCount[\s\S]*?=== 'rejected'/u);
assert.match(source, /previousAttachment && text\(previousAttachment\.status\) !== 'rejected'/u);
assert.match(source, /SET source = \$2[\s\S]*?status = 'normalized'[\s\S]*?WHERE id = \$1 AND status = 'rejected'/u);
assert.match(source, /i\.idempotency_key = \$2[\s\S]*?status: 'duplicate' as const/u);
});
test('manual API accepts exactly one bounded inline roster attachment', async () => {
const source = await readFile(new URL('../src/server.ts', import.meta.url), 'utf8');
assert.match(source, /attachments: z\.array\(encodedInputAttachmentSchema\)\.max\(1\)/);
assert.match(source, /content_base64: z\.string\(\)\.min\(4\)\.max\(70_000_000\)/);
assert.match(source, /decodeInputAttachments\(body\.attachments, config\)/);
assert.match(source, /if \(task\.status === 'parse_queued'\) void scheduleParseQueue\(\)/);
});