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

72 lines
3.2 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.

/**
* 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(',')}`);
}
});