aps-agent/scripts/pi-runtime.test.mjs

72 lines
3.2 KiB
JavaScript
Raw Normal View History

/**
* Pi 运行时用例(任务 A3.3)。
*
* 1. 未配置密钥时必须快速失败并给出指引(不是静默挂住)
* 2. 真实跑一轮:需要 APS_PI_INTEGRATION=1 且提供密钥(网络调用,默认跳过)
* —— 密钥有效时断言收到文本;无效时断言错误被归到六分类之一;两者都必须以 turn_end 收尾
*/
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { classifyPiFailure } from '@aps/server/agent/pi/runtime';
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(',')}`);
}
});