/** * Agent enrollment schema witnesses — M4-4a, the schema-level half of the * witness list in docs/plans/2026-08-29-agent-enrollment-command-design.md §5. * * Witnesses the guarantees migration 0021's tables themselves carry: the * event-type CHECK, monotonic per-agent append order (`seq`), deletion-safe * linkage (no foreign key from the events or fence tables into `agents` — * rows survive a legacy CRUD DELETE of the agent), the causation self-FK, * the outbox's FK/uniqueness/status shape, the fence's UNIQUE * (operation, key) and replay-mode CHECK, and the nullable enrollment * columns on `agents` (legacy rows insert without them). The command-level * witnesses (never-echo, same-tx atomicity, replay semantics, correlation, * CLI parity, fail-closed) belong to the M4-4b implementation slice. * * Two legs run the same witness body: * - PGlite (WASM Postgres): always runs. * - Real PostgreSQL: runs when DATABASE_URL is set — the binding witness; * CI migrates ci-postgres before `pnpm test`. */ import { randomUUID } from 'node:crypto'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { sql } from 'drizzle-orm'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { createDb } from './client.js'; import { createPgliteDb } from './client-pglite.js'; import { runPgliteMigrations } from './migrate.js'; import { agentAuditEvents, agentIdempotencyFence, agentOutbox, agents } from './schema.js'; type AnyDb = { db: { insert: (t: unknown) => { values: (v: unknown) => Promise }; execute: (q: unknown) => Promise<{ rows?: unknown[] } | unknown[]>; }; close: () => Promise; }; /** Match a constraint failure anywhere along drizzle's cause chain. */ async function expectViolation(p: Promise, re: RegExp, label = ''): Promise { let err: unknown; try { await p; } catch (e) { err = e; } expect(err, label || 'expected the statement to be refused').toBeDefined(); const messages: string[] = []; let cur: unknown = err; while (cur instanceof Error) { messages.push(cur.message); cur = (cur as { cause?: unknown }).cause; } expect(messages.join(' | '), label).toMatch(re); } function rows(res: { rows?: unknown[] } | unknown[]): Record[] { return (Array.isArray(res) ? res : (res.rows ?? [])) as Record[]; } /** Unique per-run prefix so real-PG runs never collide and clean up safely. */ const T = `agent-e-${randomUUID().slice(0, 8)}`; type EventInsert = typeof agentAuditEvents.$inferInsert; function eventRow(overrides: Partial = {}): EventInsert { return { eventType: 'agent.enrolled', actorId: `${T}-actor`, agentId: randomUUID(), correlationId: `${T}-corr-${randomUUID()}`, payload: { harness: 'claude-code', provider: 'anthropic', name: 'x', credentialMode: 'reference', }, ...overrides, }; } type FenceInsert = typeof agentIdempotencyFence.$inferInsert; function fenceRow(overrides: Partial = {}): FenceInsert { return { operation: 'agent.enroll', idempotencyKey: `${T}-${randomUUID()}`, actorId: `${T}-actor`, authorizationScope: 'platform-user', payloadDigest: `${T}-digest`, outcomeAgentId: randomUUID(), ...overrides, }; } function witnessSuite(getHandle: () => AnyDb): void { const db = () => getHandle().db as unknown as ReturnType['db']; afterAll(async () => { const d = db(); await d.execute(sql`DELETE FROM agent_outbox WHERE correlation_id LIKE ${T + '%'}`); // Caused events first: the causation self-FK is RESTRICT. await d.execute( sql`DELETE FROM agent_audit_events WHERE correlation_id LIKE ${T + '%'} AND causation_id IS NOT NULL`, ); await d.execute(sql`DELETE FROM agent_audit_events WHERE correlation_id LIKE ${T + '%'}`); await d.execute(sql`DELETE FROM agent_idempotency_fence WHERE actor_id LIKE ${T + '%'}`); await d.execute(sql`DELETE FROM agents WHERE name LIKE ${T + '%'}`); }); // ── agents: nullable enrollment columns (no backfill semantics) ──────────── it('legacy agent rows insert without enrollment columns; enrolled rows carry both', async () => { const legacyId = randomUUID(); await db() .insert(agents) .values({ id: legacyId, name: `${T}-legacy`, provider: 'anthropic', model: 'claude-fable-5', }); const legacy = rows( await db().execute(sql`SELECT harness, enrolled_at FROM agents WHERE id = ${legacyId}`), )[0]!; expect(legacy['harness']).toBeNull(); expect(legacy['enrolled_at']).toBeNull(); const enrolledId = randomUUID(); await db() .insert(agents) .values({ id: enrolledId, name: `${T}-enrolled`, provider: 'anthropic', model: 'claude-fable-5', harness: 'claude-code', enrolledAt: new Date(), }); const enrolled = rows( await db().execute(sql`SELECT harness, enrolled_at FROM agents WHERE id = ${enrolledId}`), )[0]!; expect(enrolled['harness']).toBe('claude-code'); expect(enrolled['enrolled_at']).not.toBeNull(); }); // ── agent_audit_events: CHECK, ordering, deletion-safe linkage ───────────── it('accepts both declared event types and refuses an undeclared one', async () => { await db() .insert(agentAuditEvents) .values(eventRow({ eventType: 'agent.enrolled' })); await db() .insert(agentAuditEvents) .values(eventRow({ eventType: 'agent.enrollment.replayed' })); await expectViolation( db() .insert(agentAuditEvents) .values(eventRow({ eventType: 'agent.deleted' })), /type_check|violates check/i, 'undeclared event type must be refused', ); }); it('assigns strictly increasing seq in insert order for one agent', async () => { const agentId = randomUUID(); const c1 = `${T}-seq-1-${randomUUID()}`; const c2 = `${T}-seq-2-${randomUUID()}`; await db() .insert(agentAuditEvents) .values(eventRow({ agentId, correlationId: c1 })); await db() .insert(agentAuditEvents) .values(eventRow({ agentId, eventType: 'agent.enrollment.replayed', correlationId: c2 })); const res = rows( await db().execute( sql`SELECT correlation_id, seq FROM agent_audit_events WHERE agent_id = ${agentId} ORDER BY seq ASC`, ), ); expect(res.map((r) => r['correlation_id'])).toEqual([c1, c2]); expect(Number(res[1]!['seq'])).toBeGreaterThan(Number(res[0]!['seq'])); }); it('has no foreign key into agents, and events survive agent deletion', async () => { const fks = rows( await db().execute(sql` SELECT ccu.table_name AS referenced_table FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.constraint_schema = tc.constraint_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = 'agent_audit_events' `), ); // The causation self-FK is the ONLY foreign key on the events table. expect([...new Set(fks.map((r) => r['referenced_table']))]).toEqual(['agent_audit_events']); const agentId = randomUUID(); await db() .insert(agents) .values({ id: agentId, name: `${T}-doomed`, provider: 'anthropic', model: 'claude-fable-5' }); const corr = `${T}-survive-${randomUUID()}`; await db() .insert(agentAuditEvents) .values(eventRow({ agentId, correlationId: corr })); await db().execute(sql`DELETE FROM agents WHERE id = ${agentId}`); const after = rows( await db().execute( sql`SELECT agent_id FROM agent_audit_events WHERE correlation_id = ${corr}`, ), ); expect(after).toHaveLength(1); expect(after[0]!['agent_id']).toBe(agentId); }); it('enforces the causation self-FK and RESTRICTs deleting a cause', async () => { await expectViolation( db() .insert(agentAuditEvents) .values(eventRow({ causationId: randomUUID() })), /foreign key/i, 'causation must reference an existing event', ); const causeCorr = `${T}-cause-${randomUUID()}`; await db() .insert(agentAuditEvents) .values(eventRow({ correlationId: causeCorr })); const cause = rows( await db().execute( sql`SELECT id FROM agent_audit_events WHERE correlation_id = ${causeCorr}`, ), )[0]!; await db() .insert(agentAuditEvents) .values( eventRow({ eventType: 'agent.enrollment.replayed', causationId: cause['id'] as string, }), ); await expectViolation( db().execute(sql`DELETE FROM agent_audit_events WHERE id = ${cause['id'] as string}`), /foreign key/i, 'a cause with dependent events must not be deletable', ); }); // ── agent_outbox shape ───────────────────────────────────────────────────── it('outbox rows require an existing event, one outbox row per event, closed status enum', async () => { await expectViolation( db() .insert(agentOutbox) .values({ eventId: randomUUID(), correlationId: `${T}-corr` }), /foreign key/i, 'outbox must reference an existing event', ); const corr = `${T}-ob-${randomUUID()}`; await db() .insert(agentAuditEvents) .values(eventRow({ correlationId: corr })); const event = rows( await db().execute(sql`SELECT id FROM agent_audit_events WHERE correlation_id = ${corr}`), )[0]!; const eventId = event['id'] as string; await db().insert(agentOutbox).values({ eventId, correlationId: corr }); await expectViolation( db() .insert(agentOutbox) .values({ eventId, correlationId: `${T}-ob2` }), /duplicate key|unique/i, 'one outbox record per event', ); await expectViolation( db().execute( sql`INSERT INTO agent_outbox (event_id, correlation_id, status) VALUES (${eventId}, ${`${T}-ob3`}, 'failed')`, ), /invalid input value for enum|22P02/i, 'status outside pending/processing/delivered must be refused', ); }); it('outbox FK RESTRICTs event deletion while the outbox row exists', async () => { const corr = `${T}-obr-${randomUUID()}`; await db() .insert(agentAuditEvents) .values(eventRow({ correlationId: corr })); const event = rows( await db().execute(sql`SELECT id FROM agent_audit_events WHERE correlation_id = ${corr}`), )[0]!; await db() .insert(agentOutbox) .values({ eventId: event['id'] as string, correlationId: corr }); await expectViolation( db().execute(sql`DELETE FROM agent_audit_events WHERE id = ${event['id'] as string}`), /foreign key/i, ); }); // ── agent_idempotency_fence: (operation, key) uniqueness, mode CHECK ─────── it('refuses a duplicate (operation, key) pair but allows the same key under another operation', async () => { const key = `${T}-fence-${randomUUID()}`; await db() .insert(agentIdempotencyFence) .values(fenceRow({ idempotencyKey: key })); await expectViolation( db() .insert(agentIdempotencyFence) .values(fenceRow({ idempotencyKey: key })), /duplicate key|unique/i, 'fence uniqueness is (operation, key)', ); // Same key, different operation identifier: a distinct fence. await db() .insert(agentIdempotencyFence) .values(fenceRow({ idempotencyKey: key, operation: 'agent.other' })); }); it('defaults replay mode to actor-bound and refuses an undeclared mode', async () => { const key = `${T}-mode-${randomUUID()}`; await db() .insert(agentIdempotencyFence) .values(fenceRow({ idempotencyKey: key })); const row = rows( await db().execute( sql`SELECT replay_mode FROM agent_idempotency_fence WHERE idempotency_key = ${key}`, ), )[0]!; expect(row['replay_mode']).toBe('actor-bound'); await expectViolation( db() .insert(agentIdempotencyFence) .values(fenceRow({ replayMode: 'unbound' as 'actor-bound' })), /replay_mode_check|violates check/i, 'a mode outside actor-bound/shared must be refused', ); }); it('fence has no foreign key at all, and rows survive agent deletion', async () => { const fks = rows( await db().execute(sql` SELECT ccu.table_name AS referenced_table FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.constraint_schema = tc.constraint_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = 'agent_idempotency_fence' `), ); expect(fks).toHaveLength(0); const agentId = randomUUID(); await db() .insert(agents) .values({ id: agentId, name: `${T}-fdoomed`, provider: 'anthropic', model: 'claude-fable-5', }); const key = `${T}-fsurvive-${randomUUID()}`; await db() .insert(agentIdempotencyFence) .values(fenceRow({ idempotencyKey: key, outcomeAgentId: agentId })); await db().execute(sql`DELETE FROM agents WHERE id = ${agentId}`); const after = rows( await db().execute( sql`SELECT outcome_agent_id FROM agent_idempotency_fence WHERE idempotency_key = ${key}`, ), ); expect(after).toHaveLength(1); expect(after[0]!['outcome_agent_id']).toBe(agentId); }); } // ── Leg 1: PGlite (always runs — local witness signal) ─────────────────────── describe('agent enrollment schema witnesses — PGlite', () => { let dir: string; let handle: ReturnType; beforeAll(async () => { dir = mkdtempSync(join(tmpdir(), 'agent-enroll-witness-')); handle = createPgliteDb(dir); await runPgliteMigrations(handle); }); afterAll(async () => { await handle.close(); rmSync(dir, { recursive: true, force: true }); }); witnessSuite(() => handle as unknown as AnyDb); }); // ── Leg 2: real PostgreSQL (binding witness, ci-postgres in CI) ────────────── const hasPostgres = Boolean(process.env['DATABASE_URL']); describe.skipIf(!hasPostgres)('agent enrollment schema witnesses — real PostgreSQL', () => { let handle: ReturnType; beforeAll(() => { handle = createDb(process.env['DATABASE_URL']!); }); afterAll(async () => { await handle.close(); }); witnessSuite(() => handle as unknown as AnyDb); });