aps-agent/scripts/core-invoke.test.mjs

222 lines
8.0 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.

/**
* Core.invoke 与 Provider 契约测试(任务 A2.2 / A2.3 / A2.4)。
*
* 覆盖实施设计 5 要求的六件事:Schema 校验、幂等重放、错误映射、超时与结果未知、
* 执行记录可查、契约测试套件本身能拦住坏 Provider。
*/
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { runProviderContract } from '@aps/provider-sdk/contract-suite';
import { ProviderError } from '@aps/provider-sdk';
import { createFakeProvider } from '@aps/provider-sdk/testing';
const subject = { userId: 'u-1', tenantId: 't-1' };
async function startPlatform(devProvider = true) {
if (devProvider) process.env.APS_DEV_PROVIDER = '1';
else delete process.env.APS_DEV_PROVIDER;
const { startServer } = await import('../server/dist/index.js');
const server = await startServer({ host: '127.0.0.1', port: 0, version: 'test', databasePath: ':memory:', engine: 'fake' });
return server;
}
test('未注册能力返回 invalid_argument,且不产生成功记录', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const receipt = await server.service.invoke({
subject,
sessionId: 's-test',
capabilityId: 'fake.__nope__',
params: {},
});
assert.equal(receipt.status, 'failed');
assert.equal(receipt.errorKind, 'invalid_argument');
} finally {
await server.close();
}
});
test('正常调用返回 succeeded,执行记录可查且带参数摘要', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const receipt = await server.service.invoke({
subject,
sessionId: 's-test',
capabilityId: 'fake.echo',
params: { text: '你好' },
});
assert.equal(receipt.status, 'succeeded');
assert.deepEqual(receipt.result, { echo: '你好' });
const record = server.service.core.executions.get(receipt.executionId);
assert.ok(record, '执行记录应可查');
assert.equal(record.capabilityId, 'fake.echo');
assert.equal(record.providerId, 'fake');
assert.match(record.paramsDigest, /^sha256:/, '只存参数摘要,不存明文');
assert.equal(record.status, 'succeeded');
} finally {
await server.close();
}
});
test('同一幂等键重试只执行一次,返回同一 executionId', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const request = {
subject,
sessionId: 's-test',
capabilityId: 'fake.write',
params: { value: 1 },
idempotencyKey: 'idem-core-0001',
};
const first = await server.service.invoke(request);
const second = await server.service.invoke(request);
assert.equal(first.executionId, second.executionId, '重试应返回同一执行');
assert.equal(provider.stats.calls['fake.write'], 1, 'Provider 只应被调用一次');
} finally {
await server.close();
}
});
test('超时归一为 unknown_result(结果未知,不等于失败确认)', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const receipt = await server.service.invoke({
subject,
sessionId: 's-test',
capabilityId: 'fake.slow',
params: { ms: 500 },
timeoutMs: 50,
});
assert.equal(receipt.status, 'failed');
assert.equal(receipt.errorKind, 'unknown_result');
const record = server.service.core.executions.get(receipt.executionId);
assert.equal(record?.status, 'failed');
} finally {
await server.close();
}
});
test('Provider 声明的错误分类原样透传', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const receipt = await server.service.invoke({
subject,
sessionId: 's-test',
capabilityId: 'fake.declare-error',
params: { kind: 'forbidden' },
});
assert.equal(receipt.status, 'failed');
assert.equal(receipt.errorKind, 'forbidden');
} finally {
await server.close();
}
});
test('裸异常归一为 unknown_result,不进脏分类', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const receipt = await server.service.invoke({
subject,
sessionId: 's-test',
capabilityId: 'fake.crash',
params: {},
});
assert.equal(receipt.status, 'failed');
assert.equal(receipt.errorKind, 'unknown_result');
} finally {
await server.close();
}
});
test('契约套件:假 Provider 全绿', async () => {
const report = await runProviderContract(createFakeProvider());
assert.equal(report.ok, true, JSON.stringify(report.checks.filter((check) => !check.ok), null, 2));
assert.equal(report.providerId, 'fake');
assert.ok(report.checks.length >= 7, `检查项应有若干条,实际 ${report.checks.length}`);
});
test('契约套件:坏 Provider 被拦下(负例)', async () => {
const broken = {
async describe() {
return {
id: 'broken',
displayName: '坏 Provider',
version: '0.0.0',
platformVersion: 1,
capabilities: [
{
id: 'Broken_Capability',
description: '这个描述故意写得非常长'.repeat(5),
effect: 'execute',
mode: 'batch',
inputSchema: null,
outputSchema: null,
idempotency: 'none',
errors: ['nonsense'],
},
],
};
},
async invoke() {
throw new Error('不该被调用');
},
async health() {
return { status: 'ok', checkedAt: Date.now() };
},
};
const report = await runProviderContract(broken);
assert.equal(report.ok, false, '坏 Provider 不应通过契约测试');
const failed = report.checks.filter((check) => !check.ok);
assert.ok(failed.length >= 2, `应有多项失败,实际:${failed.map((item) => item.name).join('、')}`);
const capabilityDetail = failed.find((item) => item.name.includes('声明合法'))?.detail ?? '';
for (const expected of ['id 命名不合规', 'effect 非法', 'mode 非法', '缺 inputSchema', 'errors 含非法分类']) {
assert.ok(capabilityDetail.includes(expected), `应报告「${expected}」,实际:${capabilityDetail}`);
}
assert.ok(failed.some((item) => item.name === '未知能力必须报错'), '裸异常不算合规的未知能力报错');
});
test('HTTP:注册 Provider 后能力清单非空,执行记录端点可查回执', async () => {
const server = await startPlatform(true);
try {
const capabilities = await (await fetch(`${server.url}/v1/capabilities`)).json();
assert.ok(capabilities.capabilities.length >= 5, '假 Provider 应暴露 5 个能力');
const first = capabilities.capabilities[0];
assert.equal(typeof first.providerId, 'string');
assert.ok(first.providerId.length > 0, '能力条目应带 providerId');
const receipt = await server.service.invoke({
subject,
sessionId: 's-http',
capabilityId: 'fake.echo',
params: { text: '通过 HTTP 查回执' },
});
const response = await fetch(`${server.url}/v1/executions/${receipt.executionId}`);
assert.equal(response.status, 200, '执行记录端点应返回 200');
const body = await response.json();
assert.equal(body.executionId, receipt.executionId);
assert.equal(body.status, 'succeeded');
const missing = await fetch(`${server.url}/v1/executions/exec-none`);
assert.equal(missing.status, 404, '未知执行记录应 404');
const missingBody = await missing.json();
assert.equal(missingBody.error.code, 'not_found');
} finally {
delete process.env.APS_DEV_PROVIDER;
await server.close();
}
});