// Zero-residue guard: the e2e suite must leave the dev database exactly as // it found it. globalSetup snapshots row counts, globalTeardown re-counts // and fails the run on any difference. // // Documented exception: done/failed rows in `jobs` are execution history // (the queue's audit trail); only unprocessed jobs count as residue. import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; const TABLES = [ 'users', 'departments', 'templates', 'conversations', 'messages', 'documents', 'chunks', 'doc_permissions', 'auth_sessions', 'llm_settings' ]; const SNAPSHOT = path.join(import.meta.dirname, '..', 'test-results', 'row-counts.json'); function query(sql: string): string { return execSync(`docker exec pablan-dev-postgres-1 psql -tA -U pablan -d pablan -c "${sql}"`) .toString() .trim(); } export function snapshotCounts(): Record { const counts: Record = {}; for (const table of TABLES) { counts[table] = Number(query(`SELECT count(*) FROM ${table}`)); } counts['jobs (unprocessed)'] = Number( query(`SELECT count(*) FROM jobs WHERE status IN ('pending','running')`) ); return counts; } export async function globalSetup(): Promise { fs.mkdirSync(path.dirname(SNAPSHOT), { recursive: true }); fs.writeFileSync(SNAPSHOT, JSON.stringify(snapshotCounts(), null, 2)); } export async function globalTeardown(): Promise { const before = JSON.parse(fs.readFileSync(SNAPSHOT, 'utf-8')) as Record; const after = snapshotCounts(); const diffs = Object.keys(after) .filter((key) => before[key] !== after[key]) .map((key) => ` ${key}: ${before[key]} -> ${after[key]}`); if (diffs.length > 0) { throw new Error(`e2e suite left residue in the database:\n${diffs.join('\n')}`); } }