/** * 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 { 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 }; }