import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { PERSONAL_REPORT_FAILURE_CODES } from "../src/lib/personal-report-service-core.ts"; const localMigration = readFileSync( new URL("../db/migrations/20260806000000_personal_reports.sql", import.meta.url), "utf8", ); const supabaseMigration = readFileSync( new URL("../supabase/migrations/20260806010000_personal_reports.sql", import.meta.url), "utf8", ); const localDocumentV2Migration = readFileSync( new URL("../db/migrations/20260814025000_personal_report_document_v2.sql", import.meta.url), "utf8", ); const supabaseDocumentV2Migration = readFileSync( new URL("../supabase/migrations/20260814025000_personal_report_document_v2.sql", import.meta.url), "utf8", ); const localJobsV2Migration = readFileSync( new URL("../db/migrations/20260814040000_personal_report_jobs_v2.sql", import.meta.url), "utf8", ); const supabaseJobsV2Migration = readFileSync( new URL("../supabase/migrations/20260814040000_personal_report_jobs_v2.sql", import.meta.url), "utf8", ); const localMigrations = [ "20260714000000_local_auth_compatibility.sql", "20260720000100_backend_foundation.sql", "20260721000100_self_hosted_identity.sql", "20260721000200_identity_business_bridge.sql", "20260727000000_admin_viewer_identity.sql", "20260806000000_personal_reports.sql", "20260814025000_personal_report_document_v2.sql", "20260814040000_personal_report_jobs_v2.sql", ]; const supabaseMigrations = [ "20260805030000_reconcile_rectification_v4_conversational_turns.sql", "20260806010000_personal_reports.sql", "20260814025000_personal_report_document_v2.sql", "20260814040000_personal_report_jobs_v2.sql", ]; test("migration filenames use unique, correctly ordered 14-digit versions", () => { const pattern = /^\d{14}_[a-z0-9_]+\.sql$/; assert.match("20260806000000_personal_reports.sql", pattern); assert.match("20260806010000_personal_reports.sql", pattern); for (const filename of [...localMigrations, ...supabaseMigrations]) { assert.match(filename, pattern); } for (const migrations of [localMigrations, supabaseMigrations]) { assert.deepEqual(migrations, [...migrations].sort(), "each migration stream must be ordered"); const versions = migrations.map((name) => name.split("_")[0]); assert.equal(new Set(versions).size, versions.length, "versions must be unique within a migration stream"); } }); test("document v2 and durable job migrations are exact db/supabase mirrors", () => { assert.equal(localDocumentV2Migration, supabaseDocumentV2Migration); assert.equal(localJobsV2Migration, supabaseJobsV2Migration); }); test("document v2 migration preserves v1 reads and adds explicit product depth", () => { for (const sql of [localDocumentV2Migration, supabaseDocumentV2Migration]) { assert.match(sql, /add column if not exists skill_name text/); assert.match(sql, /add column if not exists skill_version text/); assert.match(sql, /add column if not exists depth text not null default 'standard'/); assert.match(sql, /check \(depth in \('concise', 'standard', 'deep', 'research'\)\)/); assert.match(sql, /check \(schema_version in \('report_document\.v1', 'report_document\.v2'\)\)/); } }); test("durable job migration defines the complete state, lease, retry, and identity contract", () => { for (const sql of [localJobsV2Migration, supabaseJobsV2Migration]) { assert.match(sql, /create table if not exists public\.personal_report_jobs/); assert.match(sql, /status text not null default 'queued'[\s\S]*'queued',[\s\S]*'running',[\s\S]*'suspended',[\s\S]*'retrying',[\s\S]*'ready',[\s\S]*'failed',[\s\S]*'cancelled'/); assert.match(sql, /foreign key \(user_id, request_id\)[\s\S]*references public\.personal_reports \(user_id, request_id\)[\s\S]*on delete cascade/); assert.match(sql, /unique \(user_id, request_id\)/); assert.match(sql, /attempt_count between 0 and max_attempts[\s\S]*max_attempts between 1 and 10/); assert.match(sql, /status = 'running'[\s\S]*lease_token is not null[\s\S]*lease_owner is not null[\s\S]*lease_expires_at is not null[\s\S]*heartbeat_at is not null/); assert.match(sql, /create unique index if not exists personal_report_jobs_one_active_per_user[\s\S]*where status in \('queued', 'running', 'suspended', 'retrying'\)/); assert.match(sql, /personal_report_job_terminal_immutable/); assert.match(sql, /personal_report_job_identity_immutable/); assert.match(sql, /personal_report_job_invalid_transition:%s:%s/); assert.match(sql, /personal_report_job_request_fingerprint_conflict/); } }); test("durable job migration atomically enqueues reports and exposes lease RPCs only to service_role", () => { for (const sql of [localJobsV2Migration, supabaseJobsV2Migration]) { assert.match(sql, /create trigger personal_reports_90_enqueue_job[\s\S]*after insert on public\.personal_reports/); assert.match(sql, /insert into public\.personal_report_jobs[\s\S]*new\.user_id,[\s\S]*new\.request_id,[\s\S]*new\.request_fingerprint/); assert.match(sql, /from public\.personal_reports as report[\s\S]*where report\.status = 'generating'[\s\S]*on conflict \(user_id, request_id\) do nothing/); assert.match(sql, /create or replace function public\.claim_personal_report_job\(\s*p_worker_id text,\s*p_lease_seconds integer default 60,\s*p_job_id uuid default null\s*\)/); assert.match(sql, /for update skip locked/); assert.match(sql, /create or replace function public\.heartbeat_personal_report_job\(\s*p_job_id uuid,\s*p_lease_token uuid,\s*p_lease_seconds integer default 60\s*\)/); assert.match(sql, /create or replace function public\.complete_personal_report_job\(/); assert.match(sql, /v_job\.status <> 'running'[\s\S]*v_job\.lease_token is distinct from p_lease_token[\s\S]*v_job\.lease_expires_at <= v_completed_at/); assert.match(sql, /where job\.id = p_job_id[\s\S]*for update;[\s\S]*v_completed_at := clock_timestamp\(\);[\s\S]*v_job\.lease_expires_at <= v_completed_at/); assert.match(sql, /update public\.personal_reports[\s\S]*status = 'ready'[\s\S]*update public\.personal_report_jobs[\s\S]*status = 'ready'/); assert.match(sql, /create or replace function public\.fail_personal_report_job\(/); assert.match(sql, /v_job\.status <> 'running'[\s\S]*v_job\.lease_token is distinct from p_lease_token[\s\S]*v_job\.lease_expires_at <= v_failed_at/); assert.match(sql, /where job\.id = p_job_id[\s\S]*for update;[\s\S]*v_failed_at := clock_timestamp\(\);[\s\S]*v_job\.lease_expires_at <= v_failed_at/); assert.match(sql, /update public\.personal_reports[\s\S]*status = 'failed'[\s\S]*update public\.personal_report_jobs[\s\S]*status = 'failed'/); assert.match(sql, /p_skill_name is null[\s\S]*p_skill_name !~[\s\S]*p_skill_version is null[\s\S]*p_skill_version !~/); assert.match(sql, /create or replace function public\.recover_expired_personal_report_jobs\(\s*p_limit integer default 100\s*\)/); assert.match(sql, /when candidates\.report_status = 'ready' then 'ready'[\s\S]*when candidates\.report_status = 'failed' then 'failed'[\s\S]*when job\.attempt_count < job\.max_attempts then 'retrying'[\s\S]*else 'failed'/); assert.match(sql, /exhausted_reports as \([\s\S]*update public\.personal_reports[\s\S]*status = 'failed'[\s\S]*failure_code = 'calculation_unavailable'[\s\S]*candidates\.attempt_count >= candidates\.max_attempts/); assert.match(sql, /left join exhausted_reports on exhausted_reports\.id = candidates\.report_id/); assert.match(sql, /alter table public\.personal_report_jobs enable row level security/); assert.match(sql, /create policy personal_report_jobs_select_own[\s\S]*for select[\s\S]*to authenticated[\s\S]*using \(auth\.uid\(\) = user_id\)/); assert.doesNotMatch(sql, /create policy personal_report_jobs_delete_own[\s\S]*for delete/); assert.match(sql, /grant select on table public\.personal_report_jobs to authenticated/); assert.doesNotMatch(sql, /grant (?:select, )?(?:insert|update|delete)[^;]* on table public\.personal_report_jobs to authenticated/); assert.match(sql, /grant select, insert, update, delete on table public\.personal_report_jobs to service_role/); assert.match( sql, /revoke all on function public\.personal_report_enqueue_job_after_insert\(\) from public, anon, authenticated/, ); for (const signature of [ "claim_personal_report_job(text, integer, uuid)", "heartbeat_personal_report_job(uuid, uuid, integer)", "complete_personal_report_job(uuid, uuid, uuid, uuid, uuid, text, text, jsonb, text, text, text, text, text, text)", "fail_personal_report_job(uuid, uuid, uuid, uuid, uuid, text, text, text)", "recover_expired_personal_report_jobs(integer)", ]) { const escaped = signature.replace(/[()]/g, "\\$&"); assert.match(sql, new RegExp(`revoke all on function public\\.${escaped} from public, anon, authenticated`)); assert.match(sql, new RegExp(`grant execute on function public\\.${escaped} to service_role`)); } } }); test("both migrations define the same table shape with request_fingerprint after request_id", () => { const parseColumns = (sql: string) => { const body = sql.split("create table if not exists public.personal_reports")[1].split(");")[0]; const columns: string[] = []; let depth = 0; for (const raw of body.split("\n")) { const line = raw.trim(); if (!line) continue; const before = depth; for (const ch of line) { if (ch === "(") depth += 1; if (ch === ")") depth -= 1; } const first = line.split(/\s+/)[0]; if (before === 1 && /^[a-z_][a-z0-9_]*$/.test(first) && first !== "check" && first !== "unique") { columns.push(first); } } return columns; }; const expected = [ "id", "user_id", "session_id", "chart_profile_id", "request_id", "request_fingerprint", "report_type", "status", "schema_version", "presentation_mode", "requested_themes", "report_document", "calculation_hash", "evidence_hash", "skill_source_commit", "skill_snapshot_sha256", "failure_code", "created_at", "updated_at", "completed_at", ]; assert.deepEqual(parseColumns(localMigration), expected); assert.deepEqual(parseColumns(supabaseMigration), expected); }); test("request_fingerprint is not null and restricted to sha256 hex in both migrations", () => { for (const sql of [localMigration, supabaseMigration]) { assert.match(sql, /request_fingerprint text not null check \(request_fingerprint ~ '\^\[0-9a-f\]\{64\}\$'\)/); } }); test("status, report_type, presentation_mode and schema_version checks are identical", () => { for (const sql of [localMigration, supabaseMigration]) { assert.match(sql, /status text not null check \(status in \('generating', 'ready', 'failed'\)\)/); assert.match(sql, /report_type text not null check \(report_type in \('personal_full', 'personal_thematic'\)\)/); assert.match(sql, /presentation_mode text not null check \(presentation_mode in \('default', 'research'\)\)/); assert.match(sql, /schema_version text not null check \(schema_version = 'report_document\.v1'\)/); } }); test("ready requires document, completion time and both hashes; failed requires failure_code", () => { for (const sql of [localMigration, supabaseMigration]) { assert.match(sql, /check \(\(status = 'ready'\) = \(report_document is not null\)\)/); assert.match(sql, /check \(\(status = 'ready'\) = \(completed_at is not null\)\)/); assert.match(sql, /check \(\(status = 'ready'\) = \(calculation_hash is not null\)\)/); assert.match(sql, /check \(\(status = 'ready'\) = \(evidence_hash is not null\)\)/); assert.match(sql, /check \(\(status = 'failed'\) = \(failure_code is not null\)\)/); } }); test("failure_code enum in SQL matches the service constant exactly", () => { for (const sql of [localMigration, supabaseMigration]) { const block = sql.match(/failure_code text check \(failure_code in \(([\s\S]+?)\)\)/)?.[1] ?? ""; const sqlCodes = [...block.matchAll(/'([a-z_]+)'/g)].map((match) => match[1]); assert.deepEqual(sqlCodes, [...PERSONAL_REPORT_FAILURE_CODES]); } }); test("both migrations keep the (user_id, request_id) lock and one-generating-per-user index", () => { for (const sql of [localMigration, supabaseMigration]) { assert.match(sql, /unique \(user_id, request_id\)/); assert.match(sql, /create unique index if not exists personal_reports_one_generating_per_user[\s\S]*where status = 'generating'/); assert.match(sql, /references auth\.users\(id\) on delete cascade/); } }); test("RLS policies plus explicit owner grants: select/delete only, never insert/update", () => { for (const sql of [localMigration, supabaseMigration]) { assert.match(sql, /alter table public\.personal_reports enable row level security/); assert.match(sql, /create policy personal_reports_select_own[\s\S]*for select[\s\S]*to authenticated[\s\S]*using \(auth\.uid\(\) = user_id\)/); assert.match(sql, /create policy personal_reports_delete_own[\s\S]*for delete[\s\S]*to authenticated[\s\S]*using \(auth\.uid\(\) = user_id\)/); // Policies alone do not grant privileges: explicit owner grants are required. assert.match(sql, /grant select, delete on table public\.personal_reports to authenticated/); assert.doesNotMatch(sql, /grant (insert|update) on table public\.personal_reports to authenticated/); assert.match(sql, /grant select, insert, update, delete on table public\.personal_reports to service_role/); } }); test("least privilege: anon/public revoked and no direct admin_runtime body access", () => { for (const sql of [localMigration, supabaseMigration]) { assert.match(sql, /revoke all on table public\.personal_reports from public, anon, authenticated/); } assert.doesNotMatch(localMigration, /to admin_runtime/); assert.doesNotMatch(supabaseMigration, /to admin_runtime/); });