import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { startPostgresFixture } from "./helpers/postgres-fixture.ts"; const runnerPath = fileURLToPath( new URL("../scripts/db-migrate.mjs", import.meta.url), ); function dockerAvailable(): boolean { return spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { encoding: "utf8", stdio: "ignore", }).status === 0; } const skipWithoutDocker = dockerAvailable() ? false : "docker unavailable on this host"; const ids = { user: "a1111111-1111-4111-8111-111111111111", otherUser: "a2222222-2222-4222-8222-222222222222", session: "b1111111-1111-4111-8111-111111111111", otherSession: "b2222222-2222-4222-8222-222222222222", caseId: "c1111111-1111-4111-8111-111111111111", otherCase: "c2222222-2222-4222-8222-222222222222", legacyCase: "d1111111-1111-4111-8111-111111111111", }; function sqlLiteral(value: string): string { return `'${value.replaceAll("'", "''")}'`; } test("read_report_candidate_range is service_role-only, returns only the window, and stays empty without rows", { skip: skipWithoutDocker }, () => { const fixture = startPostgresFixture(); const migrate = () => spawnSync(process.execPath, [runnerPath], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl( "schema_owner", "schema-owner-test-password", ), }, }); const serviceSql = (statement: string) => fixture .psqlAs( "service_runtime", "service-runtime-test-password", `set role service_role; ${statement}`, ) .replace(/^SET\n/, ""); try { const first = migrate(); assert.equal(first.status, 0, `${first.stdout}${first.stderr}`); assert.match(first.stdout, /applied 20260904010000_read_report_candidate_range\.sql/); const second = migrate(); assert.equal(second.status, 0, `${second.stdout}${second.stderr}`); assert.match(second.stdout, /already applied 20260904010000_read_report_candidate_range\.sql/); assert.equal( fixture.psql(` select concat_ws(':', has_table_privilege('service_role', 'public.agentic_rectification_cases', 'SELECT'), has_table_privilege('service_role', 'public.agentic_rectification_cases', 'INSERT'), has_table_privilege('service_role', 'public.agentic_rectification_cases', 'UPDATE'), has_table_privilege('service_role', 'public.agentic_rectification_cases', 'DELETE'), has_function_privilege('service_role', 'public.read_report_candidate_range(uuid, uuid)', 'EXECUTE'), has_function_privilege('anon', 'public.read_report_candidate_range(uuid, uuid)', 'EXECUTE'), has_function_privilege('authenticated', 'public.read_report_candidate_range(uuid, uuid)', 'EXECUTE') ) `), "f:f:f:f:t:f:f", ); const catalog = fixture.psql(` select concat_ws(':', pg_catalog.pg_get_userbyid(p.proowner), p.prosecdef::text, coalesce(array_to_string(p.proconfig, ','), ''), p.provolatile::text ) from pg_catalog.pg_proc p join pg_catalog.pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'read_report_candidate_range' `); assert.match(catalog, /^schema_owner:true:search_path="":s$/); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", `insert into identity.users (id, name, email, email_verified, email_verified_at) values (${sqlLiteral(ids.user)}, 'Range User', 'range-user@example.com', true, now()), (${sqlLiteral(ids.otherUser)}, 'Other User', 'range-other@example.com', true, now());`, ); assert.equal( serviceSql(`select public.read_report_candidate_range(${sqlLiteral(ids.user)}::uuid) is null`), "t", ); fixture.psql(` insert into public.chat_sessions (id, user_id, title, theme, session_type, messages) values ( ${sqlLiteral(ids.session)}, ${sqlLiteral(ids.user)}, '生时校正', 'general', 'birth_time_rectification', '[]'::jsonb ), ( ${sqlLiteral(ids.otherSession)}, ${sqlLiteral(ids.otherUser)}, '生时校正', 'general', 'birth_time_rectification', '[]'::jsonb ); insert into public.agentic_rectification_cases ( id, user_id, session_id, status, skill_name, skill_version, skill_sha256, skill_source_commit, baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range ) values ( ${sqlLiteral(ids.caseId)}, ${sqlLiteral(ids.user)}, ${sqlLiteral(ids.session)}, 'candidate_accepted', 'jyotish-birth-time-rectification', '9.0.0', ${sqlLiteral("a".repeat(64))}, ${sqlLiteral("0".repeat(40))}, ${sqlLiteral("a".repeat(64))}, '{"birth_date":"1997-08-08"}'::jsonb, '{"start_time":"10:00","end_time":"10:04","secret":"must-not-leak"}'::jsonb ), ( ${sqlLiteral(ids.otherCase)}, ${sqlLiteral(ids.otherUser)}, ${sqlLiteral(ids.otherSession)}, 'candidate_accepted', 'jyotish-birth-time-rectification', '9.0.0', ${sqlLiteral("b".repeat(64))}, ${sqlLiteral("1".repeat(40))}, ${sqlLiteral("b".repeat(64))}, '{"birth_date":"1997-08-08"}'::jsonb, '{"start_time":"11:00","end_time":"11:20"}'::jsonb ); `); assert.throws( () => serviceSql(`select candidate_range from public.agentic_rectification_cases limit 1`), /permission denied for table agentic_rectification_cases/, ); assert.equal( serviceSql( `select public.read_report_candidate_range(${sqlLiteral(ids.user)}::uuid) = '{"start_time":"10:00","end_time":"10:04"}'::jsonb`, ), "t", ); const keys = serviceSql(` select array_to_string(array( select jsonb_object_keys(public.read_report_candidate_range(${sqlLiteral(ids.user)}::uuid)) order by 1 ), ',') `); assert.equal(keys, "end_time,start_time"); assert.equal( serviceSql( `select public.read_report_candidate_range(${sqlLiteral(ids.user)}::uuid) ? 'secret'`, ), "f", ); try { fixture.psql(` insert into public.birth_time_rectification_cases ( id, user_id, status, reported_date, source, candidate_start, candidate_end ) values ( ${sqlLiteral(ids.legacyCase)}, ${sqlLiteral(ids.user)}, 'confirmed', '1997-08-08', 'family_exact', '08:16', '08:20' ); `); assert.equal( serviceSql( `select public.read_report_candidate_range(${sqlLiteral(ids.user)}::uuid, ${sqlLiteral(ids.legacyCase)}::uuid) = '{"start_time":"08:16","end_time":"08:20"}'::jsonb`, ), "t", ); } catch (error) { assert.ok( false, `legacy birth_time_rectification_cases seed failed: ${error instanceof Error ? error.message : error}`, ); } } finally { fixture.stop(); } });