aps-agent/scripts/m1-loop.test.mjs

97 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* M1 闭环用例:假引擎 → 工具调用 → Core.invoke → 假 Provider → 回执回流 → 继续说话。
*
* 这条链路是 M1 的验收对象:引擎位由假引擎占着,其余(事件流、工具出口、执行记录、幂等、SSE)
* 全是真的。
*/
import assert from 'node:assert/strict';
import { test } from 'node:test';
test('假引擎发起工具调用,经 Core.invoke 到假 Provider 并把回执回流', async () => {
process.env.APS_DEV_PROVIDER = '1';
const { startServer } = await import('../server/dist/index.js');
const { createFakeRuntime } = await import('@aps/server/agent/fake-runtime');
const runtime = createFakeRuntime({
name: 'scripted',
turns: [
{
text: ['我来调用 fake.echo。'],
toolCalls: [{ capabilityId: 'fake.echo', args: { text: '你好' } }],
afterToolsText: ['调用完成。'],
},
],
});
const server = await startServer({ host: '127.0.0.1', port: 0, version: 'test', runtime });
try {
const created = await fetch(`${server.url}/v1/sessions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'project', projectId: 'p-default', title: 'M1 闭环' }),
});
const session = (await created.json()).session;
const controller = new AbortController();
const stream = await fetch(`${server.url}/v1/sessions/${session.sessionId}/events`, { headers: { accept: 'text/event-stream' }, signal: controller.signal });
const events = [];
void (async () => {
const decoder = new TextDecoder();
let buffer = '';
try {
for await (const chunk of stream.body) {
buffer += decoder.decode(chunk, { stream: true });
let index = buffer.indexOf('\n\n');
while (index >= 0) {
const frame = buffer.slice(0, index);
buffer = buffer.slice(index + 2);
const line = frame.split('\n').find((l) => l.startsWith('data: '));
if (line) events.push(JSON.parse(line.slice(6)));
index = buffer.indexOf('\n\n');
}
}
} catch {
/* aborted */
}
})();
await fetch(`${server.url}/v1/sessions/${session.sessionId}/messages`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text: '回显一下', idempotencyKey: 'idem-m1-0001' }),
});
const deadline = Date.now() + 6000;
while (Date.now() < deadline) {
if (events.some((e) => e.type === 'run.state' && e.data.state === 'idle') && events.some((e) => e.type === 'message.completed')) break;
await new Promise((resolve) => setTimeout(resolve, 20));
}
controller.abort();
const types = events.map((e) => e.type);
assert.ok(types.includes('tool.call'), `应收到 tool.call,实际:${types.join(',')}`);
assert.ok(types.includes('tool.result'), `应收到 tool.result,实际:${types.join(',')}`);
assert.equal(types.at(-1), 'run.state');
const toolCall = events.find((e) => e.type === 'tool.call');
assert.equal(toolCall.data.capabilityId, 'fake.echo');
assert.match(toolCall.data.argsDigest, /^sha256:/, '事件里只放参数摘要');
const toolResult = events.find((e) => e.type === 'tool.result');
assert.equal(toolResult.data.status, 'succeeded');
// 执行记录是平台侧权威:能在 HTTP 上查到
const execution = await (await fetch(`${server.url}/v1/executions/${toolResult.data.executionId}`)).json();
assert.equal(execution.status, 'succeeded');
assert.deepEqual(execution.result, { echo: '你好' });
// 助手回复包含工具调用前后的两段文本
const detail = await (await fetch(`${server.url}/v1/sessions/${session.sessionId}`)).json();
const assistant = detail.messages.find((m) => m.role === 'assistant');
assert.ok(assistant.text.includes('我来调用 fake.echo'), assistant.text);
assert.ok(assistant.text.includes('调用完成'), assistant.text);
} finally {
delete process.env.APS_DEV_PROVIDER;
await server.close();
}
});