diff --git a/packages/iios-service/src/capability/capability.certification.spec.ts b/packages/iios-service/src/capability/capability.certification.spec.ts new file mode 100644 index 0000000..b515bfa --- /dev/null +++ b/packages/iios-service/src/capability/capability.certification.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import type { CapabilityProvider, CapabilityRequest, ProviderResult } from '@insignia/iios-contracts'; +import { certifyProvider } from './capability.certification'; +import { SandboxProvider } from './sandbox.provider'; + +describe('adapter certification checklist (P9)', () => { + it('SandboxProvider passes certification', async () => { + const report = await certifyProvider(new SandboxProvider(['WEBHOOK'])); + expect(report.passed).toBe(true); + expect(report.checks.every((c) => c.ok)).toBe(true); + }); + + it('a broken provider fails with the specific failing checks', async () => { + const broken: CapabilityProvider = { + name: 'broken', + channelTypes: [], // no declared channels + capabilities: { canSend: false }, + async send(_req: CapabilityRequest): Promise { + throw new Error('kaboom'); // throws instead of returning FAILED + }, + }; + const report = await certifyProvider(broken); + expect(report.passed).toBe(false); + expect(report.checks.find((c) => c.name === 'declares-capabilities')?.ok).toBe(false); + expect(report.checks.find((c) => c.name === 'returns-providerRef-and-outcome')?.ok).toBe(false); + }); +}); diff --git a/packages/iios-service/src/capability/capability.certification.ts b/packages/iios-service/src/capability/capability.certification.ts new file mode 100644 index 0000000..47ec1fa --- /dev/null +++ b/packages/iios-service/src/capability/capability.certification.ts @@ -0,0 +1,67 @@ +import type { CapabilityProvider, CapabilityRequest } from '@insignia/iios-contracts'; + +export interface CertificationCheck { + name: string; + ok: boolean; + detail?: string; +} +export interface CertificationReport { + provider: string; + passed: boolean; + checks: CertificationCheck[]; +} + +/** + * The P9 adapter-certification checklist as runnable code. Before a provider is + * allowed to back a channel it must pass these — mirroring the critics' adapter + * doctrine: declare explicit capabilities, ack/return a provider ref, be + * idempotent/repeatable, tolerate edge payloads, and surface transport errors as + * FAILED rather than throwing (never swallow, never crash the egress path). + */ +export async function certifyProvider(provider: CapabilityProvider): Promise { + const checks: CertificationCheck[] = []; + const req = (over: Partial = {}): CapabilityRequest => ({ + capability: 'channel.send', + channelType: provider.channelTypes[0] ?? 'WEBHOOK', + target: 'cert@test', + payload: { text: 'certification probe' }, + idempotencyKey: 'cert-1', + ...over, + }); + + // 1. Declares explicit capabilities. + checks.push({ + name: 'declares-capabilities', + ok: provider.channelTypes.length > 0 && provider.capabilities.canSend === true, + detail: `channels=${provider.channelTypes.join(',')} canSend=${provider.capabilities.canSend}`, + }); + + // 2. Returns a providerRef + a recognized outcome (never throws on a normal send). + let normalOk = false; + try { + const r = await provider.send(req()); + normalOk = !!r.providerRef && ['SENT', 'BLOCKED', 'FAILED', 'RATE_LIMITED'].includes(r.outcome); + checks.push({ name: 'returns-providerRef-and-outcome', ok: normalOk, detail: `ref=${r.providerRef} outcome=${r.outcome}` }); + } catch (e) { + checks.push({ name: 'returns-providerRef-and-outcome', ok: false, detail: `threw: ${(e as Error).message}` }); + } + + // 3. Idempotent/repeatable — same request twice succeeds both times, no throw. + try { + const a = await provider.send(req({ idempotencyKey: 'cert-idem' })); + const b = await provider.send(req({ idempotencyKey: 'cert-idem' })); + checks.push({ name: 'repeatable', ok: !!a.providerRef && !!b.providerRef, detail: 'two sends returned refs' }); + } catch (e) { + checks.push({ name: 'repeatable', ok: false, detail: `threw: ${(e as Error).message}` }); + } + + // 4. Tolerates an empty payload (surfaces channel quirks, never crashes). + try { + const r = await provider.send(req({ payload: {}, idempotencyKey: 'cert-empty' })); + checks.push({ name: 'tolerates-empty-payload', ok: !!r.providerRef, detail: `outcome=${r.outcome}` }); + } catch (e) { + checks.push({ name: 'tolerates-empty-payload', ok: false, detail: `threw: ${(e as Error).message}` }); + } + + return { provider: provider.name, passed: checks.every((c) => c.ok), checks }; +}