/** * Hierarchy audit event + outbox schema witnesses — contract 1 §5.2 and the * schema-level half of §6.4. * * Witnesses the guarantees the tables themselves carry: verb/target-kind/ * transfer CHECK constraints, idempotency uniqueness (REQ-AUD-001 duplicate * suppression at the database level), monotonic append order (`seq`), * deletion-safe linkage (no foreign key from the events table into any class * table — events survive the deletion of their target), the causation * self-FK, and the outbox's FK/uniqueness/status shape. The repository-level * half of §6.4 (same-transaction atomicity, rollback, replay) is witnessed in * apps/gateway/src/hierarchy/hierarchy-audit.integration.test.ts. * * Two legs run the same witness body: * - PGlite (WASM Postgres): always runs. * - Real PostgreSQL (§6.8): 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 { companies, hierarchyAuditEvents, hierarchyOutbox } 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 = `hier-a-${randomUUID().slice(0, 8)}`; /** Minimal valid event row; overrides compose the negative cases. */ type EventInsert = typeof hierarchyAuditEvents.$inferInsert; function eventRow(overrides: Partial = {}): EventInsert { return { actorId: `${T}-actor`, verb: 'create', targetKind: 'company', targetId: randomUUID(), targetSnapshot: { id: 'x', slug: 'x', name: 'x', parentChain: [] }, correlationId: `${T}-corr`, idempotencyKey: `${T}-${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 hierarchy_outbox WHERE idempotency_key LIKE ${T + '%'}`); // Caused events first: the causation self-FK is RESTRICT. await d.execute( sql`DELETE FROM hierarchy_audit_events WHERE idempotency_key LIKE ${T + '%'} AND causation_id IS NOT NULL`, ); await d.execute(sql`DELETE FROM hierarchy_audit_events WHERE idempotency_key LIKE ${T + '%'}`); await d.execute(sql`DELETE FROM companies WHERE slug LIKE ${T + '%'}`); }); // ── CHECK constraints ────────────────────────────────────────────────────── it('accepts every declared verb and refuses an undeclared one', async () => { for (const verb of [ 'create', 'rename', 'delete', 'grant_create', 'grant_change', 'grant_revoke', ]) { await db().insert(hierarchyAuditEvents).values(eventRow({ verb })); } await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ verb: 'update' })), /verb_check|violates check/i, 'undeclared verb must be refused', ); }); it('refuses an undeclared target kind', async () => { await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ targetKind: 'workspace' })), /target_kind_check|violates check/i, 'workspace is not an audited target kind (workspace mutation is SOT-side)', ); }); it('requires transfer snapshots exactly on transfers', async () => { const parent = { kind: 'company', id: randomUUID(), slug: 'p' }; await db() .insert(hierarchyAuditEvents) .values( eventRow({ verb: 'transfer', targetKind: 'estate', transferFrom: parent, transferTo: { ...parent, id: randomUUID() }, }), ); await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ verb: 'transfer' })), /transfer_check|violates check/i, 'transfer without source/destination snapshots must be refused', ); await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ verb: 'transfer', transferFrom: parent })), /transfer_check|violates check/i, 'transfer with only the source snapshot must be refused', ); await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ verb: 'create', transferFrom: parent, transferTo: parent })), /transfer_check|violates check/i, 'non-transfer with transfer snapshots must be refused', ); }); // ── Idempotency and ordering ─────────────────────────────────────────────── it('refuses a duplicate idempotency key', async () => { const key = `${T}-dup-${randomUUID()}`; await db() .insert(hierarchyAuditEvents) .values(eventRow({ idempotencyKey: key })); await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ idempotencyKey: key })), /duplicate key|unique/i, ); }); it('assigns strictly increasing seq in insert order for one target', async () => { const targetId = randomUUID(); const k1 = `${T}-seq-1-${randomUUID()}`; const k2 = `${T}-seq-2-${randomUUID()}`; await db() .insert(hierarchyAuditEvents) .values(eventRow({ targetId, idempotencyKey: k1 })); await db() .insert(hierarchyAuditEvents) .values(eventRow({ targetId, verb: 'rename', idempotencyKey: k2 })); const res = rows( await db().execute( sql`SELECT idempotency_key, seq FROM hierarchy_audit_events WHERE target_id = ${targetId} ORDER BY seq ASC`, ), ); expect(res.map((r) => r['idempotency_key'])).toEqual([k1, k2]); expect(Number(res[1]!['seq'])).toBeGreaterThan(Number(res[0]!['seq'])); }); // ── Deletion-safe linkage (§5.2) ─────────────────────────────────────────── it('has no foreign key into any class table, and events survive target 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 = 'hierarchy_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(['hierarchy_audit_events']); const companyId = randomUUID(); await db() .insert(companies) .values({ id: companyId, name: 'Doomed', slug: `${T}-doomed` }); const key = `${T}-survive-${randomUUID()}`; await db() .insert(hierarchyAuditEvents) .values( eventRow({ verb: 'delete', targetId: companyId, targetSnapshot: { id: companyId, slug: `${T}-doomed`, name: 'Doomed', parentChain: [] }, idempotencyKey: key, }), ); await db().execute(sql`DELETE FROM companies WHERE id = ${companyId}`); const after = rows( await db().execute( sql`SELECT target_snapshot FROM hierarchy_audit_events WHERE idempotency_key = ${key}`, ), ); expect(after).toHaveLength(1); expect((after[0]!['target_snapshot'] as { id: string }).id).toBe(companyId); }); it('enforces the causation self-FK and RESTRICTs deleting a cause', async () => { await expectViolation( db() .insert(hierarchyAuditEvents) .values(eventRow({ causationId: randomUUID() })), /foreign key/i, 'causation must reference an existing event', ); const causeKey = `${T}-cause-${randomUUID()}`; await db() .insert(hierarchyAuditEvents) .values(eventRow({ verb: 'delete', idempotencyKey: causeKey })); const cause = rows( await db().execute( sql`SELECT id FROM hierarchy_audit_events WHERE idempotency_key = ${causeKey}`, ), )[0]!; await db() .insert(hierarchyAuditEvents) .values( eventRow({ verb: 'grant_revoke', targetKind: 'grant', causationId: cause['id'] as string }), ); await expectViolation( db().execute(sql`DELETE FROM hierarchy_audit_events WHERE id = ${cause['id'] as string}`), /foreign key/i, 'a cause with dependent events must not be deletable', ); }); // ── Outbox shape ─────────────────────────────────────────────────────────── it('outbox rows require an existing event, one outbox row per event, unique idempotency', async () => { await expectViolation( db() .insert(hierarchyOutbox) .values({ eventId: randomUUID(), idempotencyKey: `${T}-ob-${randomUUID()}`, correlationId: `${T}-corr`, }), /foreign key/i, 'outbox must reference an existing event', ); const key = `${T}-ob-${randomUUID()}`; await db() .insert(hierarchyAuditEvents) .values(eventRow({ idempotencyKey: key })); const event = rows( await db().execute(sql`SELECT id FROM hierarchy_audit_events WHERE idempotency_key = ${key}`), )[0]!; const eventId = event['id'] as string; await db() .insert(hierarchyOutbox) .values({ eventId, idempotencyKey: key, correlationId: `${T}-corr` }); await expectViolation( db() .insert(hierarchyOutbox) .values({ eventId, idempotencyKey: `${T}-ob2-${randomUUID()}`, correlationId: `${T}-c` }), /duplicate key|unique/i, 'one outbox record per event', ); await expectViolation( db().execute( sql`INSERT INTO hierarchy_outbox (event_id, idempotency_key, correlation_id, status) VALUES (${eventId}, ${`${T}-ob3-${randomUUID()}`}, 'c', '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 key = `${T}-obr-${randomUUID()}`; await db() .insert(hierarchyAuditEvents) .values(eventRow({ idempotencyKey: key })); const event = rows( await db().execute(sql`SELECT id FROM hierarchy_audit_events WHERE idempotency_key = ${key}`), )[0]!; await db() .insert(hierarchyOutbox) .values({ eventId: event['id'] as string, idempotencyKey: key, correlationId: `${T}-corr`, }); await expectViolation( db().execute(sql`DELETE FROM hierarchy_audit_events WHERE id = ${event['id'] as string}`), /foreign key/i, ); }); } // ── Leg 1: PGlite (always runs — local witness signal) ─────────────────────── describe('hierarchy audit witnesses — PGlite', () => { let dir: string; let handle: ReturnType; beforeAll(async () => { dir = mkdtempSync(join(tmpdir(), 'hier-audit-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 (§6.8 — binding witness, ci-postgres in CI) ─────── const hasPostgres = Boolean(process.env['DATABASE_URL']); describe.skipIf(!hasPostgres)('hierarchy audit witnesses — real PostgreSQL', () => { let handle: ReturnType; beforeAll(() => { handle = createDb(process.env['DATABASE_URL']!); }); afterAll(async () => { await handle.close(); }); witnessSuite(() => handle as unknown as AnyDb); });