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('durable roster replies distinguish waiting and attachment phases and select one final owner', async () => { const source = await readFile(new URL('../src/task-service.ts', import.meta.url), 'utf8'); assert.match(source, /text: intake\.message,[\s\S]*?finalReplyEligible: false/u); assert.match(source, /text: AGENTBUS_ROSTER_ATTACHMENT_RECEIVED_TEXT,[\s\S]*?finalReplyEligible: true/u); assert.match(source, /agentBusRoute: undefined[\s\S]*?agentBusRoute: input\.agentBusRoute/u); assert.match(source, /d\.payload ->> '_final_reply_eligible'[\s\S]*?newer_delivery\.created_at > d\.created_at/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\(\)/); });