feat(p9): adapter certification checklist (certifyProvider)
Task 9.4: certifyProvider runs the P9 conformance checklist a provider must pass before backing a channel — declares explicit capabilities, returns providerRef + recognized outcome (never throws), repeatable/idempotent, tolerates empty payload. 2 tests: SandboxProvider passes; a throwing/no-capability provider fails with the specific failing checks listed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<ProviderResult> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<CertificationReport> {
|
||||||
|
const checks: CertificationCheck[] = [];
|
||||||
|
const req = (over: Partial<CapabilityRequest> = {}): 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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user