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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
export * from './scope';
|
export * from './scope';
|
||||||
export * from './enums';
|
export * from './enums';
|
||||||
|
export * from './ingest';
|
||||||
export * from './ports';
|
export * from './ports';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './commands';
|
export * from './commands';
|
||||||
|
|||||||
@@ -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<string, unknown>;
|
||||||
|
};
|
||||||
|
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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestInteractionResponse {
|
||||||
|
interactionId: string;
|
||||||
|
sourceHandleId: string;
|
||||||
|
threadId: string;
|
||||||
|
state: string;
|
||||||
|
next: string[];
|
||||||
|
}
|
||||||
@@ -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:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ContextDecisionBundle> {
|
||||||
|
return {
|
||||||
|
bundleRef: 'fake-bundle',
|
||||||
|
scopeHash: 'fake-hash',
|
||||||
|
resourceSet: [],
|
||||||
|
constraints: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<SourceHandleResolution>): 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<SourceHandleResolution> {
|
||||||
|
return this.resolution;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<PolicyDecision>): 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<PolicyDecision> {
|
||||||
|
if (this.mode === 'timeout') throw new Error('opa timeout');
|
||||||
|
return this.decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<unknown[]> {
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<PlatformPrincipal>): this {
|
||||||
|
this.principal = { ...this.principal, ...p };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(): this {
|
||||||
|
this.fail = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verify(_token: string): Promise<PlatformPrincipal> {
|
||||||
|
if (this.fail) throw new Error('invalid token');
|
||||||
|
return this.principal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './fakes';
|
||||||
|
export * from './replay';
|
||||||
|
export { portalMessageBasic } from './fixtures/portal-message-basic';
|
||||||
|
export { emailFromUnknown } from './fixtures/email-from-unknown';
|
||||||
@@ -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<F>(
|
||||||
|
fixture: F,
|
||||||
|
ingest: (f: F) => Promise<{ interactionId: string }>,
|
||||||
|
countInteractions: () => Promise<number>,
|
||||||
|
): Promise<ReplayResult> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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<string, string>();
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Generated
+6
@@ -20,6 +20,12 @@ importers:
|
|||||||
|
|
||||||
packages/iios-contracts: {}
|
packages/iios-contracts: {}
|
||||||
|
|
||||||
|
packages/iios-testkit:
|
||||||
|
dependencies:
|
||||||
|
'@insignia/iios-contracts':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../iios-contracts
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.1':
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user