2026-09-28 20:21:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Pi 运行时用例(任务 A3.3)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 1. 未配置密钥时必须快速失败并给出指引(不是静默挂住)
|
|
|
|
|
|
* 2. 真实跑一轮:需要 APS_PI_INTEGRATION=1 且提供密钥(网络调用,默认跳过)
|
|
|
|
|
|
* —— 密钥有效时断言收到文本;无效时断言错误被归到六分类之一;两者都必须以 turn_end 收尾
|
|
|
|
|
|
*/
|
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
|
import { test } from 'node:test';
|
|
|
|
|
|
|
2026-09-29 10:35:40 +08:00
|
|
|
|
import { classifyPiFailure, mapPiEvent } from '@aps/server/agent/pi/runtime';
|
2026-09-28 20:21:24 +08:00
|
|
|
|
|
|
|
|
|
|
const integrationEnabled = process.env.APS_PI_INTEGRATION === '1';
|
|
|
|
|
|
const apiKey = process.env.APS_LLM_API_KEY ?? process.env.DEEPSEEK_API_KEY ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
test('未配置模型密钥时快速失败并给出配置指引', async () => {
|
|
|
|
|
|
// 环境变量也算配置来源,这里临时清掉,确保「真的没有密钥」
|
|
|
|
|
|
const saved = process.env.DEEPSEEK_API_KEY;
|
|
|
|
|
|
delete process.env.DEEPSEEK_API_KEY;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { createPiRuntime } = await import('@aps/server/agent/pi/runtime');
|
|
|
|
|
|
const runtime = createPiRuntime({ provider: 'deepseek', model: 'deepseek-chat', apiKey: '' });
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
|
() => runtime.createSession({ sessionId: 'no-key', subject: { userId: 'u', tenantId: 't' }, projectId: null, tools: [] }),
|
|
|
|
|
|
/未配置模型密钥/,
|
|
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (saved !== undefined) process.env.DEEPSEEK_API_KEY = saved;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('错误文本按六分类归一', () => {
|
|
|
|
|
|
assert.equal(classifyPiFailure('401: Authentication Fails'), 'forbidden');
|
|
|
|
|
|
assert.equal(classifyPiFailure('429 rate limit exceeded'), 'rate_limited');
|
|
|
|
|
|
assert.equal(classifyPiFailure('connect ETIMEDOUT'), 'unavailable');
|
|
|
|
|
|
assert.equal(classifyPiFailure('something odd'), 'unavailable');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('真实跑一轮:Pi 子进程接入', { skip: !integrationEnabled ? '需要 APS_PI_INTEGRATION=1 与密钥(会真实调用模型)' : false }, async () => {
|
|
|
|
|
|
const { createPiRuntime } = await import('@aps/server/agent/pi/runtime');
|
|
|
|
|
|
const runtime = createPiRuntime({ provider: 'deepseek', model: 'deepseek-chat', apiKey, turnTimeoutMs: 60_000 });
|
|
|
|
|
|
const session = await runtime.createSession({
|
|
|
|
|
|
sessionId: `it-${Date.now()}`,
|
|
|
|
|
|
subject: { userId: 'it', tenantId: 'it' },
|
|
|
|
|
|
projectId: null,
|
|
|
|
|
|
tools: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const events = [];
|
|
|
|
|
|
try {
|
|
|
|
|
|
for await (const event of session.send('只用一句话回答:1+1 等于几')) {
|
|
|
|
|
|
events.push(event);
|
|
|
|
|
|
if (event.type === 'turn_end') break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
await session.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const types = events.map((event) => event.type);
|
|
|
|
|
|
assert.ok(types.includes('turn_end'), `必须以 turn_end 收尾,实际:${types.join(',')}`);
|
|
|
|
|
|
const failed = events.find((event) => event.type === 'error');
|
|
|
|
|
|
const text = events.filter((event) => event.type === 'text').map((event) => event.delta).join('');
|
|
|
|
|
|
if (failed) {
|
|
|
|
|
|
assert.ok(
|
|
|
|
|
|
['invalid_argument', 'forbidden', 'conflict', 'rate_limited', 'unavailable', 'unknown_result'].includes(failed.kind),
|
|
|
|
|
|
`错误分类必须合法,实际 ${failed.kind}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
console.log(`(本轮模型调用失败,已按 ${failed.kind} 归一)`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
assert.ok(text.length > 0 || types.includes('message_end'), `既没有文本也没有错误,事件:${types.join(',')}`);
|
|
|
|
|
|
}
|
2026-09-29 10:35:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Pi 事件映射:工具名还原成能力 id,执行号取平台回执里的那个', () => {
|
|
|
|
|
|
const resolve = (toolName) => (toolName === 'fake_echo' ? 'fake.echo' : toolName);
|
|
|
|
|
|
|
|
|
|
|
|
const start = mapPiEvent({ type: 'tool_execution_start', toolCallId: 'call_1', toolName: 'fake_echo', args: { text: '你好' } }, resolve);
|
|
|
|
|
|
assert.deepEqual(start, [{ type: 'tool_call', callId: 'call_1', capabilityId: 'fake.echo', args: { text: '你好' } }]);
|
|
|
|
|
|
|
|
|
|
|
|
const end = mapPiEvent(
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'tool_execution_end',
|
|
|
|
|
|
toolCallId: 'call_1',
|
|
|
|
|
|
toolName: 'fake_echo',
|
|
|
|
|
|
isError: false,
|
|
|
|
|
|
result: { content: [{ type: 'text', text: '{}' }], details: { executionId: 'exec-9', status: 'succeeded', result: { echo: '你好' } } },
|
|
|
|
|
|
},
|
|
|
|
|
|
resolve,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(end[0].type, 'tool_result');
|
|
|
|
|
|
assert.equal(end[0].receipt.executionId, 'exec-9', '执行号必须取平台回执里的,而不是 Pi 的 callId');
|
|
|
|
|
|
assert.equal(end[0].receipt.status, 'succeeded');
|
|
|
|
|
|
|
|
|
|
|
|
const failed = mapPiEvent(
|
|
|
|
|
|
{ type: 'tool_execution_end', toolCallId: 'call_2', toolName: 'fake_echo', isError: true, result: { details: { executionId: 'exec-10', status: 'failed', errorKind: 'forbidden' } } },
|
|
|
|
|
|
resolve,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(failed[0].receipt.status, 'failed');
|
|
|
|
|
|
assert.equal(failed[0].receipt.errorKind, 'forbidden');
|
|
|
|
|
|
|
|
|
|
|
|
const text = mapPiEvent({ type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: '你好' } }, resolve);
|
|
|
|
|
|
assert.deepEqual(text, [{ type: 'text', delta: '你好' }]);
|
|
|
|
|
|
assert.deepEqual(mapPiEvent({ type: 'agent_settled' }, resolve), [{ type: 'turn_end', reason: 'stop' }]);
|
2026-09-28 20:21:24 +08:00
|
|
|
|
});
|