aps-agent/server/provider-sdk/contract-suite.ts

89 lines
4.2 KiB
TypeScript
Raw Permalink Normal View History

/**
* Provider 契约测试套件(任务 A2.4)。
*
* 任何 Provider 接入前都要跑这一套:能力声明是否合法、健康探测是否可用、未知能力是否报错。
* 覆盖实施设计 5 的要求:Schema 校验、幂等重放、权限拒绝、错误映射、超时与结果未知、异步受理与查询
* ——其中幂等、超时与结果未知由 Core 侧统一保证(见 scripts/core-invoke.test.mjs),
* 这里只测 Provider 自己必须满足的部分。
*/
import { ERROR_KINDS, ProviderError, isErrorKind } from './errors.js';
import type { Capability, Provider } from './index.js';
export interface ContractCheck {
name: string;
ok: boolean;
detail?: string;
}
export interface ContractReport {
providerId: string;
ok: boolean;
checks: ContractCheck[];
}
const ID_PATTERN = /^[a-z0-9]+(\.[a-z0-9-]+)+$/;
function checkCapability(capability: Capability): string[] {
const problems: string[] = [];
if (!ID_PATTERN.test(capability.id)) problems.push('id 命名不合规');
if (!capability.description || capability.description.trim() === '') problems.push('缺 description');
if (capability.description.length > 40) problems.push('description 超过 40 字');
if (!['read', 'draft', 'write'].includes(capability.effect)) problems.push('effect 非法');
if (!['sync', 'async'].includes(capability.mode)) problems.push('mode 非法');
if (!['none', 'key', 'natural'].includes(capability.idempotency)) problems.push('idempotency 非法');
if (typeof capability.inputSchema !== 'object' || capability.inputSchema === null) problems.push('缺 inputSchema');
if (typeof capability.outputSchema !== 'object' || capability.outputSchema === null) problems.push('缺 outputSchema');
if (!Array.isArray(capability.errors) || capability.errors.length === 0) problems.push('errors 不能为空');
else if (!capability.errors.every(isErrorKind)) problems.push('errors 含非法分类');
if (capability.effect === 'write' && capability.idempotency === 'none') problems.push('write 能力必须声明幂等方式');
return problems;
}
export async function runProviderContract(provider: Provider): Promise<ContractReport> {
const checks: ContractCheck[] = [];
const record = (name: string, ok: boolean, detail?: string) => {
checks.push(detail === undefined ? { name, ok } : { name, ok, detail });
};
let providerId = '(未知)';
try {
const manifest = await provider.describe();
providerId = manifest.id;
record('describe 返回 manifest', true);
record('manifest.id 非空', typeof manifest.id === 'string' && manifest.id.trim() !== '');
record('platformVersion 兼容', manifest.platformVersion === 1, `platformVersion=${manifest.platformVersion}`);
record('capabilities 非空', Array.isArray(manifest.capabilities) && manifest.capabilities.length > 0);
let bad = 0;
for (const capability of manifest.capabilities ?? []) {
const problems = checkCapability(capability);
if (problems.length > 0) {
bad += 1;
record(`能力 ${capability.id} 声明合法`, false, problems.join(';'));
}
}
if (bad === 0) record('全部能力声明合法', true);
} catch (error) {
record('describe 返回 manifest', false, error instanceof Error ? error.message : '未知错误');
}
try {
const health = await provider.health();
record('health 返回合法状态', ['ok', 'degraded', 'down'].includes(health.status), `status=${health.status}`);
} catch (error) {
record('health 返回合法状态', false, error instanceof Error ? error.message : '未知错误');
}
try {
await provider.invoke(
{ executionId: 'contract-probe', sessionId: 'contract', projectId: null, subject: { userId: 'contract', tenantId: 'contract' }, deadlineAt: Date.now() + 5_000 },
{ capabilityId: `${providerId}.__not_registered__`, params: {} },
);
record('未知能力必须报错', false, '未抛错');
} catch (error) {
record('未知能力必须报错', error instanceof ProviderError && error.kind === 'invalid_argument', error instanceof Error ? error.message : '未知错误');
}
record('错误分类集合覆盖六类', ERROR_KINDS.length === 6);
return { providerId, ok: checks.every((check) => check.ok), checks };
}