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 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", ]; const supabaseMigrations = [ "20260805030000_reconcile_rectification_v4_conversational_turns.sql", "20260806010000_personal_reports.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); assert.ok(localMigrations[5] > localMigrations[4], "local migration must sort after existing ones"); assert.ok(supabaseMigrations[1] > supabaseMigrations[0], "supabase migration must sort after existing ones"); const versions = [...localMigrations, ...supabaseMigrations].map((name) => name.split("_")[0]); assert.equal(new Set(versions).size, versions.length, "all migration versions must be unique"); }); 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/); });