import { describe, test, expect } from "bun:test"; import { readFileSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; /** * DROP-safety guard: the gateway no longer WRITES the 8 ACL columns into the * assistant DB (the gateway DB is the source of truth). This source scan fails * if any assistantDbRun(/assistantDbExec( call site writes one of those columns, * so the assistant-side DROP-COLUMN migration (302) stays safe. * * 8 ACL columns (gateway-owned, assistant-mirror dropped): * contacts.role, contacts.principal_id * contact_channels.{status, policy, verified_at, verified_via, * revoked_reason, blocked_reason} * * RESIDUAL ASSISTANT-DB ACL READS still pending before the DROP: * - gateway/src/db/data-migrations/m0006-*: one-time reconcile reads assistant * ACL columns to seed the gateway DB. * - gateway/src/db/data-migrations/m0008-*: one-time backfill reads assistant * ACL columns. * These append-only one-time migrations are the ONLY remaining assistant-DB * ACL reads; they are retired at the assistant DROP-COLUMN migration (302). * The heal/seed reads (the channel-mirror heal and the inbound contact-seed * blocked-check) are drained — they read identity/info only and resolve ACL * from the gateway DB. The WRITE side is clean — this guard asserts that. */ // Deliberate, greppable exceptions. Must stay empty: every entry is an // assistant-DB ACL write that the DROP migration would break. const ACL_WRITE_ALLOWLIST: string[] = []; const ACL_COLUMNS = [ "role", "principal_id", "status", "policy", "verified_at", "verified_via", "revoked_reason", "blocked_reason", ] as const; // Info columns are assistant-owned and intentionally NOT flagged. const INFO_COLUMNS = ["last_seen_at", "interaction_count", "last_interaction"]; const GATEWAY_SRC = join(import.meta.dirname!, ".."); const EXCLUDED_DIRS = new Set(["__tests__", "data-migrations"]); const EXCLUDED_FILES = new Set(["assistant-db-proxy.ts"]); function collectSourceFiles(dir: string): string[] { const out: string[] = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); const st = statSync(full); if (st.isDirectory()) { if (EXCLUDED_DIRS.has(entry)) continue; out.push(...collectSourceFiles(full)); continue; } if (!entry.endsWith(".ts")) continue; if (entry.endsWith(".test.ts")) continue; if (EXCLUDED_FILES.has(entry)) continue; out.push(full); } return out; } /** * Extract the SQL string literal argument of an assistantDbRun/assistantDbExec * call starting at `callStart`. Handles template literals and quoted strings * spanning multiple lines. Returns the literal text (without delimiters) and * the index just past it, or null if no string literal follows the open paren. */ function extractSqlArg( src: string, callStart: number, ): { sql: string; end: number } | null { const open = src.indexOf("(", callStart); if (open === -1) return null; let i = open + 1; // Skip whitespace to the first argument. while (i < src.length && /\s/.test(src[i])) i++; const quote = src[i]; if (quote !== "`" && quote !== '"' && quote !== "'") return null; const start = i + 1; i = start; while (i < src.length) { if (src[i] === "\\") { i += 2; continue; } if (src[i] === quote) { return { sql: src.slice(start, i), end: i + 1 }; } i++; } return null; } // Only writes to these tables carry the dropped ACL columns. status/policy // etc. on other tables (channel_verification_sessions, *_ingress_invites) are // unrelated and must not be flagged. const ACL_TABLES = ["contacts", "contact_channels"]; /** True if `sql` writes one of the 8 ACL columns into contacts/contact_channels. */ function sqlWritesAclColumn(sql: string): boolean { const normalized = sql.replace(/\s+/g, " ").toLowerCase(); // INSERT INTO