244 lines
9.1 KiB
JavaScript
244 lines
9.1 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
}); |