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