89 lines
4.2 KiB
TypeScript
89 lines
4.2 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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 };
|
|||
|
|
}
|