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

97 lines
4.1 KiB
JavaScript
Raw Normal View History

/**
* 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', databasePath: ':memory:', engine: 'fake', 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();
}
});