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

122 lines
4.6 KiB
JavaScript
Raw Permalink Normal View History

/**
* 能力桥用例(任务 A3.4 / A3.11)。
*
* 不启动 Pi:把扩展装到一个假的 Pi 注册表上,直接调它注册出来的工具,
* 验证「Pi 工具调用 → 平台内部端点 → Core.invoke → 假 Provider → 回执」这条链路。
* 真引擎那一环另有一条需要 APS_PI_INTEGRATION=1 的用例(见 pi-runtime.test.mjs)。
*/
import assert from 'node:assert/strict';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import { pathToFileURL } from 'node:url';
const EXTENSION = pathToFileURL(join(process.cwd(), 'server/agent/pi/extension/aps-bridge.ts')).href;
async function startPlatform() {
process.env.APS_DEV_PROVIDER = '1';
const { startServer } = await import('../server/dist/index.js');
return startServer({ host: '127.0.0.1', port: 0, version: 'test', databasePath: ':memory:', engine: 'fake' });
}
/** 用假 Pi 加载扩展,返回注册出来的工具表 */
async function loadBridge(configPath) {
const previous = process.env.APS_BRIDGE_CONFIG;
process.env.APS_BRIDGE_CONFIG = configPath;
try {
const module = await import(EXTENSION);
const tools = new Map();
module.default({
registerTool(tool) {
tools.set(tool.name, tool);
},
});
return tools;
} finally {
if (previous === undefined) delete process.env.APS_BRIDGE_CONFIG;
else process.env.APS_BRIDGE_CONFIG = previous;
}
}
async function writeBridgeConfig(server, sessionId, capabilities) {
const dir = await mkdtemp(join(tmpdir(), 'aps-bridge-'));
const file = join(dir, 'bridge.json');
await writeFile(
file,
JSON.stringify({ baseUrl: server.url, internalToken: server.internalToken, sessionId, capabilities }),
'utf8',
);
return { dir, file };
}
test('扩展把平台能力注册成 Pi 工具,调用经 Core.invoke 返回回执', async () => {
const server = await startPlatform();
const created = await (
await fetch(`${server.url}/v1/sessions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'project', projectId: 'p-default', title: '桥用例' }),
})
).json();
const sessionId = created.session.sessionId;
const { dir, file } = await writeBridgeConfig(server, sessionId, [
{
toolName: 'fake_echo',
capabilityId: 'fake.echo',
description: '回显输入',
parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
},
]);
try {
const tools = await loadBridge(file);
assert.ok(tools.has('fake_echo'), `应注册 fake_echo,实际:${[...tools.keys()].join(',')}`);
const tool = tools.get('fake_echo');
const result = await tool.execute('call-abc', { text: '你好' });
const receipt = JSON.parse(result.content[0].text);
assert.equal(receipt.status, 'succeeded');
assert.deepEqual(receipt.result, { echo: '你好' });
// 平台侧留下执行记录,且能经 HTTP 查到
const execution = await (await fetch(`${server.url}/v1/executions/${receipt.executionId}`)).json();
assert.equal(execution.status, 'succeeded');
const record = server.service.core.executions.get(receipt.executionId);
assert.equal(record.sessionId, sessionId, '执行记录应归属发起会话');
assert.equal(record.capabilityId, 'fake.echo');
} finally {
await server.close();
await rm(dir, { recursive: true, force: true });
delete process.env.APS_DEV_PROVIDER;
}
});
test('能力不存在时扩展抛错,Pi 会把它当工具错误(不会伪造成功)', async () => {
const server = await startPlatform();
const created = await (
await fetch(`${server.url}/v1/sessions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'project', projectId: 'p-default', title: '失败路径' }),
})
).json();
const { dir, file } = await writeBridgeConfig(server, created.session.sessionId, [
{ toolName: 'fake_nope', capabilityId: 'fake.__nope__', description: '不存在的能力', parameters: { type: 'object' } },
]);
try {
const tools = await loadBridge(file);
await assert.rejects(() => tools.get('fake_nope').execute('call-bad', {}), /执行失败|invalid_argument/);
} finally {
await server.close();
await rm(dir, { recursive: true, force: true });
delete process.env.APS_DEV_PROVIDER;
}
});
test('没有桥配置时不注册任何工具(宁可说做不到,也不给坏工具)', async () => {
const tools = await loadBridge(join(tmpdir(), 'not-exists-bridge.json'));
assert.equal(tools.size, 0);
});