72 lines
3.2 KiB
JavaScript
72 lines
3.2 KiB
JavaScript
/**
|
||
* 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(',')}`);
|
||
}
|
||
}); |