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

106 lines
3.6 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.

/**
* 回合执行器按工具执行模式分流(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');
});