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

122 lines
4.6 KiB
JavaScript
Raw Permalink 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.

/**
* 能力桥用例(任务 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);
});