/** * 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(); } });