Files
LWLT-AI/mock-business-system/external-agent-client.test.mjs
2026-07-13 19:57:46 +08:00

429 lines
16 KiB
JavaScript

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));
});
}