import assert from 'node:assert/strict'; import { request as httpRequest } from 'node:http'; import { afterEach, describe, it } from 'node:test'; import { ExternalAgentParser, collectSseOutput, normalizeParseResult, parseJsonObject, parseSseBlock } from './external-agent-client.mjs'; import { createBusinessServer } from './server.mjs'; const servers = []; afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(resolve)))); }); describe('external parser contract', () => { it('creates a task-isolated session and sends the raw input through SSE', async () => { const calls = []; const fetchImpl = async (url, options) => { calls.push({ url, options, body: options.body ? JSON.parse(options.body) : null }); if (url.endsWith('/api/open/agent-sessions')) { return new Response(JSON.stringify({ session_id: 'open_sess_test', status: 'active' }), { status: 200, headers: { 'content-type': 'application/json' } }); } const fragments = [ '{"status":"agent_parse_passed","blockers":[],"operation":{"action":"team_order_create","data":{"product":{"name":"遇见老挝"}}', '}}' ]; const sse = fragments.map((fragment) => ( `event: message.delta\ndata: ${JSON.stringify({ content: fragment })}\n\n` )).join(''); return new Response(sse, { status: 200, headers: { 'content-type': 'text/event-stream' } }); }; const parser = new ExternalAgentParser({ baseUrl: 'https://superagent.nianxx.cn', apiKey: 'df_open_test', fetchImpl, now: () => new Date('2026-07-12T08:00:00.000Z'), csrfToken: 'csrf-test-token' }); const result = await parser.parse({ taskId: 'TASK-001', rawText: '产品名称:遇见老挝', receivedAt: '2026-07-12T08:00:00.000Z' }); assert.equal(result.status, 'agent_parse_passed'); assert.equal(result.operation.action, 'team_order_create'); assert.equal(result.operation.data.product.name, '遇见老挝'); assert.equal(result.operation.submit_mode, 'dry_run'); assert.equal(result.operation.source.task_id, 'TASK-001'); assert.equal(result.session_id, 'open_sess_test'); assert.equal(result.external_request.service, 'external_parse_api'); assert.equal(result.external_request.transport, 'sse'); assert.equal(result.external_request.stage, 'stream_completed'); assert.equal(result.external_request.session_id_present, true); assert.equal(result.external_request.event_count, 2); assert.equal(calls.length, 2); assert.equal(calls[0].options.headers.Authorization, 'Bearer df_open_test'); assert.equal(calls[0].options.headers['X-CSRF-Token'], 'csrf-test-token'); assert.equal(calls[0].options.headers.Cookie, 'csrf_token=csrf-test-token'); assert.deepEqual(calls[0].body, { external_subject_id: 'business-task:TASK-001', idempotency_key: 'business-parser-session:TASK-001', metadata: { source: 'business_system', module: 'input_parser', task_id: 'TASK-001' } }); assert.equal(calls[1].url, 'https://superagent.nianxx.cn/api/open/agent-sessions/open_sess_test/messages/stream'); assert.equal(calls[1].options.headers.Accept, 'text/event-stream'); assert.equal(calls[1].body.idempotency_key, 'business-parser-message:TASK-001'); assert.match(calls[1].body.message, /产品名称:遇见老挝/); assert.equal(calls[1].body.metadata.session_id, 'open_sess_test'); }); it('returns a blocked result without making a request when the server key is absent', async () => { let requestCount = 0; const parser = new ExternalAgentParser({ baseUrl: 'https://superagent.nianxx.cn', apiKey: '', fetchImpl: async () => { requestCount += 1; throw new Error('should not be called'); } }); const result = await parser.parse({ taskId: 'TASK-002', rawText: '测试输入' }); assert.equal(result.status, 'agent_parse_blocked'); assert.equal(result.error_code, 'external_service_not_configured'); assert.equal(result.external_request.stage, 'not_configured'); assert.equal(result.external_request.session_id_present, false); assert.equal(requestCount, 0); assert.match(result.blockers[0], /DEERFLOW_OPEN_API_KEY/); }); it('probes the provider health endpoint without creating a session', async () => { const calls = []; const parser = new ExternalAgentParser({ baseUrl: 'https://superagent.nianxx.cn', apiKey: 'df_open_test', fetchImpl: async (url, options) => { calls.push({ url, options }); return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'content-type': 'application/json' } }); } }); const result = await parser.checkConnection({ timeoutMs: 100 }); assert.equal(result.ok, true); assert.equal(result.authenticated, null); assert.equal(result.authentication_checked, false); assert.equal(result.http_status, 200); assert.equal(calls.length, 1); assert.equal(calls[0].url, 'https://superagent.nianxx.cn/health'); assert.equal(calls[0].options.method, 'GET'); assert.equal(calls[0].options.body, undefined); assert.equal(calls[0].options.headers.Authorization, 'Bearer df_open_test'); }); it('reports an unhealthy provider endpoint as disconnected', async () => { const parser = new ExternalAgentParser({ baseUrl: 'https://superagent.nianxx.cn', apiKey: 'df_open_invalid', fetchImpl: async () => new Response(JSON.stringify({ detail: 'unauthorized' }), { status: 401, headers: { 'content-type': 'application/json' } }) }); const result = await parser.checkConnection({ timeoutMs: 100 }); assert.equal(result.ok, false); assert.equal(result.authenticated, null); assert.equal(result.error_code, 'external_health_unhealthy'); }); it('maps external authorization and active-run errors without exposing the key', async () => { const parser = new ExternalAgentParser({ baseUrl: 'https://superagent.nianxx.cn', apiKey: 'df_open_secret_value', fetchImpl: async () => new Response(JSON.stringify({ detail: 'Missing scope: agent_sessions:message; df_open_secret_value' }), { status: 403, headers: { 'content-type': 'application/json' } }) }); const result = await parser.parse({ taskId: 'TASK-003', rawText: '测试输入' }); assert.equal(result.status, 'agent_parse_blocked'); assert.equal(result.error_code, 'external_forbidden'); assert.equal(result.external_status, 403); assert.match(result.blockers[0], /权限不足/); assert.doesNotMatch(JSON.stringify(result), /df_open_secret_value/); }); it('returns a bounded cancellation result when an SSE body ignores cancel()', async () => { const hangingBody = { async *[Symbol.asyncIterator]() { await new Promise(() => {}); }, cancel() { return new Promise(() => {}); } }; const parser = new ExternalAgentParser({ baseUrl: 'https://superagent.nianxx.cn', apiKey: 'df_open_test', timeoutMs: 2_000, totalTimeoutMs: 2_000, fetchImpl: async (url) => { if (url.endsWith('/api/open/agent-sessions')) { return new Response(JSON.stringify({ session_id: 'open_sess_hanging' }), { status: 200 }); } return { status: 200, ok: true, body: hangingBody }; } }); const controller = new AbortController(); const abortTimer = setTimeout(() => controller.abort(), 20); const startedAt = Date.now(); try { const result = await parser.parse({ taskId: 'TASK-CANCEL', rawText: '测试输入', signal: controller.signal }); assert.equal(result.status, 'agent_parse_blocked'); assert.equal(result.error_code, 'external_cancelled'); assert.ok(Date.now() - startedAt < 1_500); } finally { clearTimeout(abortTimer); } }); }); describe('SSE and result helpers', () => { it('parses CRLF SSE blocks and JSON objects wrapped in markdown', () => { assert.deepEqual(parseSseBlock('event: run.completed\r\ndata: {"status":"agent_parse_passed"}\r\n'), { event: 'run.completed', data: { status: 'agent_parse_passed' } }); assert.deepEqual(parseJsonObject('```json\n{"status":"agent_parse_blocked","blockers":[]}\n```'), { status: 'agent_parse_blocked', blockers: [] }); assert.deepEqual(parseSseBlock('event: done\ndata: [DONE]\n\n'), { event: 'done', data: null, done: true }); }); it('returns after a terminal SSE event even if the provider keeps the stream open', async () => { const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode( 'event: run.completed\ndata: {"status":"agent_parse_blocked","blockers":["test"]}\n\n' )); } }); let timer; try { const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('SSE terminal event was not honored')), 200); }); const collected = await Promise.race([collectSseOutput(body), timeout]); assert.equal(collected.eventCount, 1); assert.deepEqual(collected.result, { status: 'agent_parse_blocked', blockers: ['test'] }); } finally { clearTimeout(timer); await body.cancel().catch(() => {}); } }); it('accepts a structured final event and preserves the operation contract', async () => { const body = new Response( 'event: run.completed\ndata: {"status":"agent_parse_passed","blockers":[],"operation":{"action":"team_order_create","data":{}}}\n\n', { headers: { 'content-type': 'text/event-stream' } } ).body; const collected = await collectSseOutput(body); const result = normalizeParseResult(collected.result, { taskId: 'TASK-004', receivedAt: '2026-07-12T08:00:00.000Z', sessionId: 'open_sess_test' }); assert.equal(collected.eventCount, 1); assert.equal(result.status, 'agent_parse_passed'); assert.equal(result.session_id, 'open_sess_test'); assert.equal(result.operation.source.parser_prompt_version, 'external-profile'); assert.deepEqual(result.operation.data.attachments, []); }); it('collects assistant chunks from the documented messages event shape', async () => { const events = [ [{ type: 'human', content: 'ignore this input' }, { type: 'metadata', source: 'run' }], [{ type: 'AIMessageChunk', content: '{"status":"agent_parse_passed","operation":' }], [{ type: 'AIMessageChunk', content: '{"action":"team_order_create","data":{}}}' }] ].map((data) => `event: messages\ndata: ${JSON.stringify(data)}\n\n`).join(''); const collected = await collectSseOutput(new Response(events).body); assert.equal(collected.eventCount, 3); assert.equal(parseJsonObject(collected.text).status, 'agent_parse_passed'); assert.equal(parseJsonObject(collected.text).operation.action, 'team_order_create'); }); it('adapts the currently published flat parser shape into the business operation contract', () => { const result = normalizeParseResult({ order_mode: '团队-单个下单', order_nature: '测试', product_name: '测试产品', departure_date: '2099-01-01', adult: 2, child_with_bed: 1, child_without_bed: 0, infant: 0, tour_leader: 0, op: '测试 OP', sales: '测试销售', remarks: '仅测试解析', test_marker: 'TEST-001' }, { taskId: 'TASK-006', receivedAt: '2026-07-12T08:00:00.000Z' }); assert.equal(result.status, 'agent_parse_passed'); assert.equal(result.operation.order_nature, 'test'); assert.equal(result.operation.data.product.name, '测试产品'); assert.deepEqual(result.operation.data.departure_dates, ['2099-01-01']); assert.equal(result.operation.data.passenger_counts.expected_total, 3); assert.equal(result.operation.data.op_user.name, '测试 OP'); assert.equal(result.operation.data.sales_user.name, '测试销售'); }); it('accepts camelCase flat fields emitted by the external Profile', () => { const result = normalizeParseResult({ action: 'team_order_create', orderMode: '团队-单个下单', orderNature: '测试', productName: '测试产品', departureDate: '2099-01-01', adults: 1, childrenWithBed: 2, childrenWithoutBed: 1, infants: 0, tourLeader: 0, operator: '测试 OP', salesperson: '测试销售', remarks: '仅测试解析' }, { taskId: 'TASK-007', receivedAt: '2026-07-12T08:00:00.000Z' }); assert.equal(result.status, 'agent_parse_passed'); assert.equal(result.operation.action, 'team_order_create'); assert.equal(result.operation.data.passenger_counts.expected_total, 4); assert.equal(result.operation.data.op_user.name, '测试 OP'); assert.equal(result.operation.data.sales_user.name, '测试销售'); }); it('blocks a generic Profile response instead of inventing an operation', () => { const result = normalizeParseResult({ reply: '请补充订单信息', intent: 'order_help', missingFields: ['product'] }, { taskId: 'TASK-008', receivedAt: '2026-07-12T08:00:00.000Z' }); assert.equal(result.status, 'agent_parse_blocked'); assert.equal(result.error_code, 'external_result_contract_mismatch'); assert.deepEqual(result.response_keys, ['reply', 'intent', 'missingFields']); assert.equal(result.operation, null); }); }); describe('business-system HTTP boundary', () => { it('returns the real AI connectivity probe from /api/status', async () => { const parser = { async checkConnection() { return { ok: true, configured: true, reachable: true, authenticated: true, http_status: 422, probe: 'validation_response' }; }, async parse() { throw new Error('not used'); } }; const server = createBusinessServer({ parser }); servers.push(server); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); const response = await requestJson(address.port, 'GET', '/api/status'); assert.equal(response.statusCode, 200); assert.equal(response.body.ai_configured, true); assert.equal(response.body.ai_connected, true); assert.equal(response.body.ai_probe.http_status, 422); }); it('exposes /api/parse and no longer exposes /api/agent routes', async () => { const seen = []; const parser = { async parse(payload) { seen.push(payload); return { status: 'agent_parse_passed', blockers: [], operation: { action: 'team_order_create', data: {} } }; } }; const server = createBusinessServer({ parser }); servers.push(server); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); const response = await requestJson(address.port, 'POST', '/api/parse', { task_id: 'TASK-005', raw_text: '测试输入', received_at: '2026-07-12T08:00:00.000Z' }); assert.equal(response.statusCode, 200); assert.equal(response.body.operation.action, 'team_order_create'); assert.deepEqual(seen, [{ rawText: '测试输入', taskId: 'TASK-005', receivedAt: '2026-07-12T08:00:00.000Z' }]); const oldRoute = await requestJson(address.port, 'POST', '/api/agent/parse', {}); assert.equal(oldRoute.statusCode, 405); }); }); function requestJson(port, method, path, body) { return new Promise((resolve, reject) => { const request = httpRequest({ host: '127.0.0.1', port, method, path, headers: { 'Content-Type': 'application/json' } }, (response) => { const chunks = []; response.on('data', (chunk) => chunks.push(chunk)); response.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); let parsed; try { parsed = text ? JSON.parse(text) : null; } catch (error) { parsed = text; } resolve({ statusCode: response.statusCode, body: parsed }); }); }); request.on('error', reject); request.end(body === undefined ? undefined : JSON.stringify(body)); }); }