97 lines
4.0 KiB
JavaScript
97 lines
4.0 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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();
|
|||
|
|
}
|
|||
|
|
});
|