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