aps-agent/scripts/turn-runner.test.mjs

106 lines
3.6 KiB
JavaScript
Raw Normal View History

/**
* 回合执行器按工具执行模式分流(A3.4)。
*
* 两种模式必须都覆盖,否则 Pi 那条路只有真模型测试才碰得到:
* platform —— 平台执行并回填(假引擎走这条)
* runtime —— 引擎自己执行(Pi + 扩展桥),平台只观察事件、不得重复执行
*/
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { runTurn } from '@aps/server/app/turn-runner';
function stubSession(mode, events) {
return {
id: 'stub',
toolExecution: mode,
async *send() {
for (const event of events) yield event;
},
resolveToolResult() {},
abort() {},
async dispose() {},
};
}
const toolCallEvents = [
{ type: 'text', delta: '我来调用。' },
{ type: 'tool_call', callId: 'c-1', capabilityId: 'test.echo', args: { q: 1 } },
{ type: 'tool_result', callId: 'c-1', receipt: { executionId: 'exec-1', status: 'succeeded' } },
{ type: 'turn_end', reason: 'stop' },
];
function collect() {
const events = [];
return { events, publish: (event) => events.push(event) };
}
test('runtime 模式:平台不执行工具,只观察 tool.call 与 tool.result', async () => {
const { events, publish } = collect();
let invocations = 0;
const result = await runTurn({
runtimeSession: stubSession('runtime', toolCallEvents),
sessionId: 's-1',
runId: 'r-1',
assistantMessageId: 'm-1',
text: '调用试试',
publish,
invoke: async () => {
invocations += 1;
return { executionId: 'platform-should-not-run', status: 'succeeded' };
},
signal: new AbortController().signal,
});
assert.equal(invocations, 0, '引擎模式下平台不得再执行一次(否则等于重复调用)');
const types = events.map((event) => event.type);
assert.deepEqual(types, ['run.state', 'message.delta', 'tool.call', 'tool.result', 'message.completed', 'run.state']);
assert.equal(result.toolCalls, 1);
assert.equal(result.finishReason, 'stop');
});
test('platform 模式:平台执行并把回执回填给运行时', async () => {
const { events, publish } = collect();
const resolved = [];
const session = stubSession('platform', toolCallEvents);
session.resolveToolResult = (callId, receipt) => resolved.push({ callId, receipt });
let invocations = 0;
await runTurn({
runtimeSession: session,
sessionId: 's-1',
runId: 'r-1',
assistantMessageId: 'm-1',
text: '调用试试',
publish,
invoke: async () => {
invocations += 1;
return { executionId: 'exec-platform', status: 'succeeded' };
},
signal: new AbortController().signal,
});
assert.equal(invocations, 1, '平台模式必须执行一次');
assert.deepEqual(resolved, [{ callId: 'c-1', receipt: { executionId: 'exec-platform', status: 'succeeded' } }]);
const types = events.map((event) => event.type);
assert.ok(types.includes('tool.result'), types.join(','));
});
test('错误回合的结束原因不会被 turn_end 覆盖回 stop', async () => {
const { events, publish } = collect();
const result = await runTurn({
runtimeSession: stubSession('runtime', [
{ type: 'error', kind: 'forbidden', message: '401 无效密钥' },
{ type: 'turn_end', reason: 'stop' },
]),
sessionId: 's-1',
runId: 'r-1',
assistantMessageId: 'm-1',
text: '触发错误',
publish,
invoke: async () => ({ executionId: 'x', status: 'succeeded' }),
signal: new AbortController().signal,
});
assert.equal(result.finishReason, 'error');
assert.equal(events.find((event) => event.type === 'message.completed').data.finishReason, 'error');
});