From 169c1a375cdaa887e21f0e9d542f21cb8cd10230 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Tue, 30 Jun 2026 21:05:41 +0530 Subject: [PATCH] feat(testkit): deterministic fake platform ports + fixtures + replay oracle Fakes for all 7 IiosPlatformPorts (deny/timeout/ambiguous controls), portal + unknown-email fixtures, replayTwice idempotency oracle. Adds ingest request contract. Vitest alias resolves workspace packages to source. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/iios-contracts/src/index.ts | 1 + packages/iios-contracts/src/ingest.ts | 42 ++++++++++++ packages/iios-testkit/package.json | 15 +++++ packages/iios-testkit/src/fakes/capability.ts | 10 +++ packages/iios-testkit/src/fakes/cmp.ts | 20 ++++++ packages/iios-testkit/src/fakes/crre.ts | 15 +++++ packages/iios-testkit/src/fakes/index.ts | 34 ++++++++++ packages/iios-testkit/src/fakes/mdm.ts | 34 ++++++++++ packages/iios-testkit/src/fakes/opa.ts | 39 +++++++++++ packages/iios-testkit/src/fakes/sas.ts | 10 +++ packages/iios-testkit/src/fakes/session.ts | 29 +++++++++ .../src/fixtures/email-from-unknown.ts | 13 ++++ .../src/fixtures/portal-message-basic.ts | 13 ++++ packages/iios-testkit/src/index.ts | 4 ++ packages/iios-testkit/src/replay.ts | 37 +++++++++++ packages/iios-testkit/src/testkit.test.ts | 64 +++++++++++++++++++ packages/iios-testkit/tsconfig.json | 10 +++ pnpm-lock.yaml | 6 ++ vitest.config.ts | 18 ++++++ 19 files changed, 414 insertions(+) create mode 100644 packages/iios-contracts/src/ingest.ts create mode 100644 packages/iios-testkit/package.json create mode 100644 packages/iios-testkit/src/fakes/capability.ts create mode 100644 packages/iios-testkit/src/fakes/cmp.ts create mode 100644 packages/iios-testkit/src/fakes/crre.ts create mode 100644 packages/iios-testkit/src/fakes/index.ts create mode 100644 packages/iios-testkit/src/fakes/mdm.ts create mode 100644 packages/iios-testkit/src/fakes/opa.ts create mode 100644 packages/iios-testkit/src/fakes/sas.ts create mode 100644 packages/iios-testkit/src/fakes/session.ts create mode 100644 packages/iios-testkit/src/fixtures/email-from-unknown.ts create mode 100644 packages/iios-testkit/src/fixtures/portal-message-basic.ts create mode 100644 packages/iios-testkit/src/index.ts create mode 100644 packages/iios-testkit/src/replay.ts create mode 100644 packages/iios-testkit/src/testkit.test.ts create mode 100644 packages/iios-testkit/tsconfig.json create mode 100644 vitest.config.ts diff --git a/packages/iios-contracts/src/index.ts b/packages/iios-contracts/src/index.ts index 62e427a..1719f25 100644 --- a/packages/iios-contracts/src/index.ts +++ b/packages/iios-contracts/src/index.ts @@ -1,5 +1,6 @@ export * from './scope'; export * from './enums'; +export * from './ingest'; export * from './ports'; export * from './events'; export * from './commands'; diff --git a/packages/iios-contracts/src/ingest.ts b/packages/iios-contracts/src/ingest.ts new file mode 100644 index 0000000..5ddc988 --- /dev/null +++ b/packages/iios-contracts/src/ingest.ts @@ -0,0 +1,42 @@ +import type { ScopeVector } from './scope'; +import type { HandleKind, InteractionKind, MessagePartKind } from './enums'; + +/** + * The ingest API contract (Data Model §11 OpenAPI `IngestInteractionRequest`). + * A trusted host app or channel adapter posts this to `/v1/interactions/ingest`. + */ +export interface IngestInteractionRequest { + scope: ScopeVector; + channel: { + type: string; // PORTAL, EMAIL, WHATSAPP, ... + externalChannelId?: string; + capabilityContract?: Record; + }; + source: { + handleKind: HandleKind; + externalId: string; + displayName?: string; + }; + kind?: InteractionKind; + thread?: { + externalThreadId?: string; + subject?: string; + }; + parts: Array<{ + kind: MessagePartKind; + bodyText?: string; + contentRef?: string; + mimeType?: string; + }>; + occurredAt: string; + providerEventId?: string; + metadata?: Record; +} + +export interface IngestInteractionResponse { + interactionId: string; + sourceHandleId: string; + threadId: string; + state: string; + next: string[]; +} diff --git a/packages/iios-testkit/package.json b/packages/iios-testkit/package.json new file mode 100644 index 0000000..55b3844 --- /dev/null +++ b/packages/iios-testkit/package.json @@ -0,0 +1,15 @@ +{ + "name": "@insignia/iios-testkit", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@insignia/iios-contracts": "workspace:*" + } +} diff --git a/packages/iios-testkit/src/fakes/capability.ts b/packages/iios-testkit/src/fakes/capability.ts new file mode 100644 index 0000000..36d9270 --- /dev/null +++ b/packages/iios-testkit/src/fakes/capability.ts @@ -0,0 +1,10 @@ +import type { IiosPlatformPorts } from '@insignia/iios-contracts'; + +type CapabilityPort = IiosPlatformPorts['capability']; + +/** Deterministic Capability Broker fake. Reports a successful provider send. */ +export class FakeCapability implements CapabilityPort { + async invoke(_input: unknown): Promise<{ providerRef: string; outcome: string }> { + return { providerRef: 'fake-provider', outcome: 'SENT' }; + } +} diff --git a/packages/iios-testkit/src/fakes/cmp.ts b/packages/iios-testkit/src/fakes/cmp.ts new file mode 100644 index 0000000..2ef94a2 --- /dev/null +++ b/packages/iios-testkit/src/fakes/cmp.ts @@ -0,0 +1,20 @@ +import type { IiosPlatformPorts } from '@insignia/iios-contracts'; + +type CmpPort = IiosPlatformPorts['cmp']; + +/** Deterministic CMP (consent) fake. Defaults to NOT_REQUIRED. */ +export class FakeCmp implements CmpPort { + private status: 'ALLOW' | 'DENY' | 'NOT_REQUIRED' = 'NOT_REQUIRED'; + + setStatus(s: 'ALLOW' | 'DENY' | 'NOT_REQUIRED'): this { + this.status = s; + return this; + } + + async checkPurpose(_input: unknown): Promise<{ receiptRef?: string; status: 'ALLOW' | 'DENY' | 'NOT_REQUIRED' }> { + return { + receiptRef: this.status === 'ALLOW' ? 'fake-receipt' : undefined, + status: this.status, + }; + } +} diff --git a/packages/iios-testkit/src/fakes/crre.ts b/packages/iios-testkit/src/fakes/crre.ts new file mode 100644 index 0000000..4139ed5 --- /dev/null +++ b/packages/iios-testkit/src/fakes/crre.ts @@ -0,0 +1,15 @@ +import type { IiosPlatformPorts, ContextDecisionBundle } from '@insignia/iios-contracts'; + +type CrrePort = IiosPlatformPorts['crre']; + +/** Deterministic CRRE fake. Returns an empty (no-constraint) context bundle. */ +export class FakeCrre implements CrrePort { + async resolve(_input: unknown): Promise { + return { + bundleRef: 'fake-bundle', + scopeHash: 'fake-hash', + resourceSet: [], + constraints: [], + }; + } +} diff --git a/packages/iios-testkit/src/fakes/index.ts b/packages/iios-testkit/src/fakes/index.ts new file mode 100644 index 0000000..33465e4 --- /dev/null +++ b/packages/iios-testkit/src/fakes/index.ts @@ -0,0 +1,34 @@ +import type { IiosPlatformPorts } from '@insignia/iios-contracts'; +import { FakeSession } from './session'; +import { FakeOpa } from './opa'; +import { FakeCmp } from './cmp'; +import { FakeMdm } from './mdm'; +import { FakeCrre } from './crre'; +import { FakeSas } from './sas'; +import { FakeCapability } from './capability'; + +export { FakeSession, FakeOpa, FakeCmp, FakeMdm, FakeCrre, FakeSas, FakeCapability }; + +/** The full set of fakes, typed so tests can reach each fake's control methods. */ +export interface FakePorts extends IiosPlatformPorts { + session: FakeSession; + opa: FakeOpa; + cmp: FakeCmp; + mdm: FakeMdm; + crre: FakeCrre; + sas: FakeSas; + capability: FakeCapability; +} + +/** Assemble a fresh, fully-faked IiosPlatformPorts (all default to "happy"). */ +export function makeFakePorts(): FakePorts { + return { + session: new FakeSession(), + opa: new FakeOpa(), + cmp: new FakeCmp(), + mdm: new FakeMdm(), + crre: new FakeCrre(), + sas: new FakeSas(), + capability: new FakeCapability(), + }; +} diff --git a/packages/iios-testkit/src/fakes/mdm.ts b/packages/iios-testkit/src/fakes/mdm.ts new file mode 100644 index 0000000..7b4a4c0 --- /dev/null +++ b/packages/iios-testkit/src/fakes/mdm.ts @@ -0,0 +1,34 @@ +import type { IiosPlatformPorts, SourceHandleResolution } from '@insignia/iios-contracts'; + +type MdmPort = IiosPlatformPorts['mdm']; + +/** + * Deterministic MDM fake. Defaults to UNRESOLVED (an unknown handle stays a + * SourceHandle — no silent merge). Can be set to ambiguous or resolved. + */ +export class FakeMdm implements MdmPort { + private resolution: SourceHandleResolution = { + canonicalEntityId: undefined, + confidence: 0, + status: 'UNRESOLVED', + }; + + setResolution(r: Partial): this { + this.resolution = { ...this.resolution, ...r }; + return this; + } + + ambiguous(confidence = 0.42): this { + this.resolution = { canonicalEntityId: undefined, confidence, status: 'AMBIGUOUS' }; + return this; + } + + resolved(canonicalEntityId: string, confidence = 0.99): this { + this.resolution = { canonicalEntityId, confidence, status: 'RESOLVED' }; + return this; + } + + async resolveSourceHandle(_input: unknown): Promise { + return this.resolution; + } +} diff --git a/packages/iios-testkit/src/fakes/opa.ts b/packages/iios-testkit/src/fakes/opa.ts new file mode 100644 index 0000000..314e292 --- /dev/null +++ b/packages/iios-testkit/src/fakes/opa.ts @@ -0,0 +1,39 @@ +import type { IiosPlatformPorts, PolicyDecision } from '@insignia/iios-contracts'; + +type OpaPort = IiosPlatformPorts['opa']; + +/** Deterministic OPA fake. Defaults to allow; can be set to deny or timeout. */ +export class FakeOpa implements OpaPort { + private decision: PolicyDecision = { + decisionRef: 'fake-allow', + allow: true, + obligations: [], + ttlSeconds: 60, + }; + private mode: 'normal' | 'timeout' = 'normal'; + + setDecision(d: Partial): this { + this.decision = { ...this.decision, ...d }; + return this; + } + + deny(reason = 'denied by fake opa'): this { + this.decision = { + decisionRef: 'fake-deny', + allow: false, + obligations: [{ kind: 'AUDIT', reason }], + ttlSeconds: 0, + }; + return this; + } + + timeout(): this { + this.mode = 'timeout'; + return this; + } + + async decide(_input: unknown): Promise { + if (this.mode === 'timeout') throw new Error('opa timeout'); + return this.decision; + } +} diff --git a/packages/iios-testkit/src/fakes/sas.ts b/packages/iios-testkit/src/fakes/sas.ts new file mode 100644 index 0000000..34bbe38 --- /dev/null +++ b/packages/iios-testkit/src/fakes/sas.ts @@ -0,0 +1,10 @@ +import type { IiosPlatformPorts } from '@insignia/iios-contracts'; + +type SasPort = IiosPlatformPorts['sas']; + +/** Deterministic SAS (secure-store) fake. Pass-through tokenization in dev. */ +export class FakeSas implements SasPort { + async tokenizeParts(parts: unknown[]): Promise { + return parts; + } +} diff --git a/packages/iios-testkit/src/fakes/session.ts b/packages/iios-testkit/src/fakes/session.ts new file mode 100644 index 0000000..c38232f --- /dev/null +++ b/packages/iios-testkit/src/fakes/session.ts @@ -0,0 +1,29 @@ +import type { IiosPlatformPorts, PlatformPrincipal } from '@insignia/iios-contracts'; + +type SessionPort = IiosPlatformPorts['session']; + +/** Deterministic Session fake. Returns a fixed principal; can be set to reject. */ +export class FakeSession implements SessionPort { + private principal: PlatformPrincipal = { + principalRef: 'fake-principal', + orgId: 'org_demo', + appId: 'portal-demo', + assurance: 'authenticated', + }; + private fail = false; + + setPrincipal(p: Partial): this { + this.principal = { ...this.principal, ...p }; + return this; + } + + reject(): this { + this.fail = true; + return this; + } + + async verify(_token: string): Promise { + if (this.fail) throw new Error('invalid token'); + return this.principal; + } +} diff --git a/packages/iios-testkit/src/fixtures/email-from-unknown.ts b/packages/iios-testkit/src/fixtures/email-from-unknown.ts new file mode 100644 index 0000000..29be7f8 --- /dev/null +++ b/packages/iios-testkit/src/fixtures/email-from-unknown.ts @@ -0,0 +1,13 @@ +import type { IngestInteractionRequest } from '@insignia/iios-contracts'; + +/** An email from an unrecognised address — the handle must stay UNRESOLVED. */ +export const emailFromUnknown: IngestInteractionRequest = { + scope: { orgId: 'org_demo', appId: 'portal-demo' }, + channel: { type: 'EMAIL', externalChannelId: 'support-inbox' }, + source: { handleKind: 'EMAIL', externalId: 'someone@example.com', displayName: 'Someone' }, + kind: 'EMAIL', + thread: { externalThreadId: 'thr_email_1', subject: 'Question about my plot' }, + parts: [{ kind: 'TEXT', bodyText: 'Hello, I have a question.' }], + occurredAt: '2026-06-30T15:12:00.000Z', + providerEventId: 'evt_email_1', +}; diff --git a/packages/iios-testkit/src/fixtures/portal-message-basic.ts b/packages/iios-testkit/src/fixtures/portal-message-basic.ts new file mode 100644 index 0000000..f58a9de --- /dev/null +++ b/packages/iios-testkit/src/fixtures/portal-message-basic.ts @@ -0,0 +1,13 @@ +import type { IngestInteractionRequest } from '@insignia/iios-contracts'; + +/** A known portal user sends a plain text message into a thread. */ +export const portalMessageBasic: IngestInteractionRequest = { + scope: { orgId: 'org_demo', appId: 'portal-demo' }, + channel: { type: 'PORTAL', externalChannelId: 'portal-web' }, + source: { handleKind: 'PORTAL_USER', externalId: 'user_rahul', displayName: 'Rahul' }, + kind: 'MESSAGE', + thread: { externalThreadId: 'thr_demo_1', subject: 'Payment help' }, + parts: [{ kind: 'TEXT', bodyText: 'my payment failed' }], + occurredAt: '2026-06-30T15:10:00.000Z', + providerEventId: 'evt_portal_1', +}; diff --git a/packages/iios-testkit/src/index.ts b/packages/iios-testkit/src/index.ts new file mode 100644 index 0000000..f303a51 --- /dev/null +++ b/packages/iios-testkit/src/index.ts @@ -0,0 +1,4 @@ +export * from './fakes'; +export * from './replay'; +export { portalMessageBasic } from './fixtures/portal-message-basic'; +export { emailFromUnknown } from './fixtures/email-from-unknown'; diff --git a/packages/iios-testkit/src/replay.ts b/packages/iios-testkit/src/replay.ts new file mode 100644 index 0000000..22354b2 --- /dev/null +++ b/packages/iios-testkit/src/replay.ts @@ -0,0 +1,37 @@ +/** + * Replay oracle (Bottom-Up hard gate: "replay the same fixture twice and prove + * no duplicate business state"). Ingests a fixture, counts, ingests it AGAIN, + * counts again. The caller asserts: same interaction id, unchanged count. + * + * The `ingest` function the caller supplies must derive its idempotency key + * deterministically from the fixture (e.g. its providerEventId) so the second + * run is a true duplicate. + */ +export interface ReplayResult { + firstId: string; + secondId: string; + countAfterFirst: number; + countAfterSecond: number; +} + +export async function replayTwice( + fixture: F, + ingest: (f: F) => Promise<{ interactionId: string }>, + countInteractions: () => Promise, +): Promise { + const first = await ingest(fixture); + const countAfterFirst = await countInteractions(); + const second = await ingest(fixture); + const countAfterSecond = await countInteractions(); + return { + firstId: first.interactionId, + secondId: second.interactionId, + countAfterFirst, + countAfterSecond, + }; +} + +/** Convenience predicate: true when a replay produced no new business state. */ +export function isIdempotent(r: ReplayResult): boolean { + return r.firstId === r.secondId && r.countAfterFirst === r.countAfterSecond; +} diff --git a/packages/iios-testkit/src/testkit.test.ts b/packages/iios-testkit/src/testkit.test.ts new file mode 100644 index 0000000..61dd5c8 --- /dev/null +++ b/packages/iios-testkit/src/testkit.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { + makeFakePorts, + replayTwice, + isIdempotent, + portalMessageBasic, +} from './index'; +import { PolicyDeniedError, type IngestInteractionRequest } from '@insignia/iios-contracts'; + +describe('fake platform ports', () => { + it('OPA defaults to allow, can be set to deny, and can time out', async () => { + const ports = makeFakePorts(); + expect((await ports.opa.decide({})).allow).toBe(true); + + ports.opa.deny('nope'); + const denied = await ports.opa.decide({}); + expect(denied.allow).toBe(false); + expect(denied.obligations[0]?.reason).toBe('nope'); + + ports.opa.timeout(); + await expect(ports.opa.decide({})).rejects.toThrow('opa timeout'); + }); + + it('MDM keeps an unknown handle UNRESOLVED, and can be made ambiguous', async () => { + const ports = makeFakePorts(); + expect((await ports.mdm.resolveSourceHandle({})).status).toBe('UNRESOLVED'); + + ports.mdm.ambiguous(0.42); + const res = await ports.mdm.resolveSourceHandle({}); + expect(res.status).toBe('AMBIGUOUS'); + expect(res.canonicalEntityId).toBeUndefined(); + expect(res.confidence).toBe(0.42); + }); + + it('fail-closed pattern: a denied OPA decision yields PolicyDeniedError', async () => { + const ports = makeFakePorts(); + ports.opa.deny(); + const decision = await ports.opa.decide({}); + // This is the guard the kernel will apply (Task 1.3). + const guard = () => { + if (!decision.allow) throw new PolicyDeniedError(decision.obligations[0]?.reason); + }; + expect(guard).toThrow(PolicyDeniedError); + }); +}); + +describe('replay oracle', () => { + it('replaying a fixture twice creates no duplicate business state', async () => { + // In-memory "kernel": dedupes on the fixture's providerEventId. + const store = new Map(); + let seq = 0; + const ingest = async (f: IngestInteractionRequest) => { + const key = f.providerEventId ?? 'no-key'; + if (!store.has(key)) store.set(key, `int_${++seq}`); + return { interactionId: store.get(key)! }; + }; + const countInteractions = async () => store.size; + + const result = await replayTwice(portalMessageBasic, ingest, countInteractions); + expect(isIdempotent(result)).toBe(true); + expect(result.countAfterFirst).toBe(1); + expect(result.countAfterSecond).toBe(1); + }); +}); diff --git a/packages/iios-testkit/tsconfig.json b/packages/iios-testkit/tsconfig.json new file mode 100644 index 0000000..52ce13a --- /dev/null +++ b/packages/iios-testkit/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e23777..073555d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: packages/iios-contracts: {} + packages/iios-testkit: + dependencies: + '@insignia/iios-contracts': + specifier: workspace:* + version: link:../iios-contracts + packages: '@esbuild/aix-ppc64@0.28.1': diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..58019ef --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; +import { fileURLToPath } from 'node:url'; + +const r = (p: string) => fileURLToPath(new URL(p, import.meta.url)); + +export default defineConfig({ + resolve: { + alias: { + // Resolve workspace packages to their TS source so tests run without a + // build step. Production builds still go through tsc/dist. + '@insignia/iios-contracts': r('./packages/iios-contracts/src/index.ts'), + '@insignia/iios-testkit': r('./packages/iios-testkit/src/index.ts'), + }, + }, + test: { + include: ['packages/**/src/**/*.{test,spec}.ts'], + }, +});