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

244 lines
9.1 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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('同一幂等键重试不重复生效:写操作先开确认卡,确认后只执行一次', async () => {
const provider = createFakeProvider();
const server = await startPlatform(false);
try {
await server.service.registerProvider(provider);
const request = {
subject,
sessionId: 's-test',
projectId: 'p-default',
capabilityId: 'fake.write',
params: { value: 1 },
idempotencyKey: 'idem-core-0001',
};
// A4:写操作不会直接执行,先落一张待确认动作(协议第 7 节)
const first = await server.service.invoke(request);
assert.equal(first.status, 'pending-confirmation');
assert.ok(first.approvalId, '要给出确认卡标识');
const second = await server.service.invoke(request);
assert.equal(second.approvalId, first.approvalId, '同一幂等键重试返回同一张确认卡');
assert.equal(provider.stats.calls['fake.write'], undefined, '没有确认就不该碰业务系统');
const approved = await server.service.core.approve(subject, first.approvalId, {
decision: 'approve',
revision: 7,
idempotencyKey: 'idem-approve-0001',
});
assert.equal(approved.kind, 'ok');
assert.equal(approved.receipt.status, 'succeeded');
assert.equal(provider.stats.calls['fake.write'], 1, '确认后执行一次');
const again = await server.service.core.approve(subject, first.approvalId, {
decision: 'approve',
revision: 7,
idempotencyKey: 'idem-approve-0002',
});
assert.equal(again.receipt.executionId, approved.receipt.executionId, '重复确认返回同一执行');
assert.equal(provider.stats.calls['fake.write'], 1, '同一写请求重试不重复生效');
} 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}`, { headers: { 'x-aps-user': subject.userId, 'x-aps-tenant': subject.tenantId } });
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`, { headers: { 'x-aps-user': subject.userId } });
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();
}
});