import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { spawnSync } from "node:child_process"; import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; import pg from "pg"; import { startPostgresFixture, type PostgresFixture } from "./helpers/postgres-fixture.ts"; const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url)); const migrationPath = new URL("../supabase/migrations/20260925020000_chart_subject_typed_columns.sql", import.meta.url); const migrationSql = readFileSync(migrationPath, "utf8"); const docker = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { stdio: "ignore" }).status === 0; const schemaPassword = "schema-owner-test-password"; const HASH = "a".repeat(64); function windowsMigrationDirectory(): string { const directory = mkdtempSync(join(tmpdir(), "jyotisha-subject-migrations-")); const seen = new Map(); for (const relative of ["../db/migrations", "../supabase/migrations"]) { const source = fileURLToPath(new URL(relative, import.meta.url)); for (const name of readdirSync(source)) { if (!/^\d{14}_[a-z0-9_]+\.sql$/.test(name)) continue; const bytes = readFileSync(join(source, name)); if (/^\.\.\/.+\.sql$/.test(bytes.toString("utf8").trim())) continue; const previous = seen.get(name); if (previous) { assert.equal(bytes.equals(previous), true, `${name} mirrors must match`); continue; } seen.set(name, bytes); writeFileSync(join(directory, name), bytes); } } assert.ok(seen.has("20260925020000_chart_subject_typed_columns.sql")); return directory; } function applyMigrations(fixture: PostgresFixture): void { const env = { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", schemaPassword), }; const first = spawnSync(process.execPath, [runner], { encoding: "utf8", env }); if (first.status === 0) return; const detail = `${first.stderr ?? ""}\n${first.stdout ?? ""}`; assert.match(detail, /duplicate migration filename/, detail); const directory = windowsMigrationDirectory(); try { const second = spawnSync(process.execPath, [runner], { encoding: "utf8", env: { ...env, MIGRATIONS_DIRECTORY: directory }, }); assert.equal(second.status, 0, second.stderr || second.stdout); } finally { rmSync(directory, { force: true, recursive: true }); } } function lastLine(value: string): string { const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); return lines[lines.length - 1] ?? ""; } function backfillUpdate(): string { const start = migrationSql.indexOf("-- BEGIN chart_subject_backfill"); const end = migrationSql.indexOf("-- END chart_subject_backfill"); assert.ok(start >= 0 && end > start, "backfill markers missing"); const update = migrationSql.slice(start, end).match(/update public\.chart_profiles as subject[\s\S]*?;/)?.[0]; assert.ok(update, "backfill update must be cut from the migration"); return update; } function profileJson(extra: Record = {}): string { return JSON.stringify({ name: "虚构乙", date: "1992-06-15", reportedTime: "07:40", birthTimeSource: "family_exact", birthTimeStatus: "reported", birthPlaceLabel: "虚构港", latitude: 22.3, longitude: 114.1, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "lahiri", chartRelationship: "friend", ...extra, }).replaceAll("'", "''"); } test("corrective migration clears untrusted clocks and serializes concurrent fourth/fifth people", { skip: docker ? false : "docker unavailable", }, async () => { const fixture = startPostgresFixture(); const first = new pg.Client({ connectionString: fixture.connectionUrl("schema_owner", schemaPassword) }); const second = new pg.Client({ connectionString: fixture.connectionUrl("schema_owner", schemaPassword) }); try { applyMigrations(fixture); const owner = randomUUID(); fixture.psql(`insert into identity.users (id,name,email,email_verified,email_verified_at) values ('${owner}','Fictional concurrency','${owner}@example.invalid',true,now());`); const ids = [randomUUID(), randomUUID(), randomUUID()]; for (const id of ids) fixture.psql(`insert into public.chart_profiles(id,user_id,role,profile) values ('${id}','${owner}','other','${profileJson()}'::jsonb);`); const before = fixture.psql(`select row_to_json(p)::text from public.profiles p where id='${owner}'`); fixture.psql(`alter table public.chart_profiles disable trigger chart_profiles_guard_birth; update public.chart_profiles set birth_time_status='confirmed', active_birth_time='08:00', active_birth_date='1992-06-14', active_birth_timezone_offset=8 where id='${ids[0]}'; alter table public.chart_profiles enable trigger chart_profiles_guard_birth;`); const correction = readFileSync(new URL("../supabase/migrations/20260925030000_chart_subject_trust_and_limit.sql", import.meta.url), "utf8"); fixture.psql(correction); assert.equal(fixture.psql(`select birth_time_status || '|' || (active_birth_time is null and active_birth_date is null and active_birth_timezone_offset is null)::text || '|' || to_char(reported_birth_time,'HH24:MI') from public.chart_profiles where id='${ids[0]}'`), "reported|true|07:40"); assert.equal(fixture.psql(`select row_to_json(p)::text from public.profiles p where id='${owner}'`), before); await Promise.all([first.connect(), second.connect()]); await first.query("begin"); await second.query("begin"); await first.query("insert into public.chart_profiles(user_id,role,profile) values($1,'other',$2)", [owner, profileJson()]); let finished = false; const fifth = second.query("insert into public.chart_profiles(user_id,role,profile) values($1,'other',$2)", [owner, profileJson()]) .then(() => ({ error: "" }), (error: Error) => ({ error: error.message })).finally(() => { finished = true; }); await new Promise((resolve) => setTimeout(resolve, 120)); assert.equal(finished, false, "fifth insert waits for the same owner's transaction lock"); await first.query("commit"); assert.match((await fifth).error, /subject_limit_reached/); await second.query("rollback"); assert.equal(fixture.psql(`select count(*) from public.chart_profiles where user_id='${owner}'`), "4"); } finally { await Promise.all([first.end(), second.end()]); fixture.stop(); } }); test("sessions GET self uses real PostgreSQL IS and returns only null/self bindings", { skip: docker ? false : "docker unavailable", }, () => { const fixture = startPostgresFixture(); try { applyMigrations(fixture); const owner = randomUUID(); const other = randomUUID(); fixture.psql(`insert into identity.users(id,name,email,email_verified,email_verified_at) values('${owner}','Fictional sessions','${owner}@example.invalid',true,now()); insert into public.chat_sessions(user_id,title,theme,messages,chart_profile_id) values ('${owner}','Fictional legacy','general','[{"role":"user","text":"fixture"}]',null), ('${owner}','Fictional self','general','[{"role":"user","text":"fixture"}]','self'), ('${owner}','Fictional other','general','[{"role":"user","text":"fixture"}]','${other}');`); const script = ` import { mock } from 'node:test'; import { createLocalPostgresDataClient, closeLocalPostgresDataPools } from './src/lib/db/local-postgres-client-core.ts'; const local = createLocalPostgresDataClient(${JSON.stringify(fixture.connectionUrl("app_runtime", "app-runtime-test-password"))}, {id:${JSON.stringify(owner)}}); mock.module('server-only', {namedExports:{}}); mock.module('@/lib/supabase/server', {namedExports:{createServerSupabaseClient:async()=>({from:local.from.bind(local),auth:{getUser:async()=>({data:{user:{id:${JSON.stringify(owner)}}},error:null})}})}}); const {GET}=await import('./src/app/api/sessions/route.ts'); const response=await GET(new Request('https://example.invalid/api/sessions?subject=self')); console.log(JSON.stringify({status:response.status,body:await response.json()})); await closeLocalPostgresDataPools(); `; const result = spawnSync(process.execPath, ["--experimental-test-module-mocks", "--import", "tsx", "--input-type=module", "--eval", script], { encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); const response = JSON.parse(result.stdout.trim().split("\n").at(-1) || "{}"); assert.equal(response.status, 200); assert.deepEqual(response.body.sessions.map((row: { chart_profile_id: string | null }) => row.chart_profile_id).sort(), [null, "self"].sort()); } finally { fixture.stop(); } }); test("chart subject columns, guards, limit and cascaded delete", { skip: docker ? false : "docker unavailable", }, () => { const fixture = startPostgresFixture(); try { applyMigrations(fixture); assert.match(migrationSql, /delete from public\.chart_profiles where role = 'self'/); assert.doesNotMatch(migrationSql, /drop column .*profile/i); const owner = randomUUID(); const otherUser = randomUUID(); fixture.psql(` insert into identity.users (id, name, email, email_verified, email_verified_at) values ('${owner}', 'Subject Owner', '${owner}@example.invalid', true, now()), ('${otherUser}', 'Subject Other', '${otherUser}@example.invalid', true, now()); `); const dirtyId = randomUUID(); fixture.psql(` insert into public.chart_profiles (id, user_id, role, profile) values ('${dirtyId}', '${owner}', 'other', '{"name":"虚构丙","chartRelationship":"family"}'::jsonb); `); assert.equal( fixture.psql(`select birth_date is null and reported_birth_time is null from public.chart_profiles where id = '${dirtyId}'`), "t", ); const completeId = randomUUID(); fixture.psql(` insert into public.chart_profiles (id, user_id, role, profile, name, birth_date, reported_birth_time, latitude, longitude) values ( '${completeId}', '${owner}', 'other', '${profileJson()}'::jsonb, null, null, null, null, null ); `); fixture.psql(backfillUpdate()); const filled = fixture.psql(` select name || '|' || birth_date::text || '|' || to_char(reported_birth_time, 'HH24:MI') || '|' || birth_time_source || '|' || latitude::text || '|' || ayanamsa from public.chart_profiles where id = '${completeId}' `); assert.equal(filled, "虚构乙|1992-06-15|07:40|family_exact|22.3|lahiri"); assert.equal( fixture.psql(`select profile::text like '%chartRelationship%' from public.chart_profiles where id = '${completeId}'`), "t", "jsonb is kept for rollback and is not the reason a relationship column exists", ); for (let index = 0; index < 2; index += 1) { fixture.psql(` insert into public.chart_profiles (id, user_id, role, profile) values ('${randomUUID()}', '${owner}', 'other', '${profileJson({ name: `虚构${index}` })}'::jsonb); `); } assert.throws( () => fixture.psql(` insert into public.chart_profiles (id, user_id, role, profile) values ('${randomUUID()}', '${owner}', 'other', '${profileJson({ name: "虚构超额" })}'::jsonb); `), /subject_limit_reached/, ); assert.throws( () => fixture.psql(` insert into public.chart_profiles (id, user_id, role, profile) values ('${randomUUID()}', '${owner}', 'self', '{}'::jsonb); `), /chart_subject_self_mirror_retired/, ); assert.throws( () => fixture.psql(` update public.chart_profiles set reported_birth_time = '08:01' where id = '${completeId}'; `), /reported_birth_time_is_immutable/, ); assert.throws( () => fixture.psql(` update public.chart_profiles set active_birth_time = '08:02' where id = '${completeId}'; `), /chart_subject_active_birth_is_locked/, ); assert.throws( () => fixture.psql(` update public.chart_profiles set birth_time_status = 'confirmed' where id = '${completeId}'; `), /chart_subject_birth_status_locked/, ); const sessionId = randomUUID(); const reportId = randomUUID(); const requestId = randomUUID(); fixture.psql(` insert into public.chat_sessions (id, user_id, title, theme, messages, chart_profile_id) values ('${sessionId}', '${owner}', '虚构对话', 'general', '[]'::jsonb, '${completeId}'); insert into public.personal_reports ( id, user_id, chart_profile_id, request_id, request_fingerprint, report_type, status, schema_version, presentation_mode, requested_themes, depth, skill_name, skill_version, skill_source_commit, skill_snapshot_sha256 ) values ( '${reportId}', '${owner}', '${completeId}', '${requestId}', '${HASH}', 'personal_full', 'generating', 'report_document.v2', 'default', array['career']::text[], 'standard', 'jyotish-personal-report', '1.0.0', '${"b".repeat(40)}', '${HASH}' ); `); const keeper = randomUUID(); fixture.psql(` insert into public.chat_sessions (id, user_id, title, theme, messages) values ('${keeper}', '${owner}', '本人对话', 'general', '[{"role":"user","text":"hi"}]'::jsonb); `); const usage = fixture.psqlAs( "app_runtime", "app-runtime-test-password", `set role authenticated; select set_config('request.jwt.claim.sub', '${owner}', true); select session_count::text || '|' || report_count::text from public.chart_subject_usage('${completeId}');`, ); assert.equal(lastLine(usage), "1|1"); const hidden = fixture.psqlAs( "app_runtime", "app-runtime-test-password", `set role authenticated; select set_config('request.jwt.claim.sub', '${otherUser}', true); select count(*) from public.chart_profiles where id = '${completeId}';`, ); assert.equal(lastLine(hidden), "0"); assert.throws( () => fixture.psqlAs( "app_runtime", "app-runtime-test-password", `set role authenticated; select set_config('request.jwt.claim.sub', '${otherUser}', true); select public.delete_chart_subject('${completeId}');`, ), /chart_subject_not_found/, ); assert.equal(fixture.psql(`select count(*) from public.chart_profiles where id = '${completeId}'`), "1"); fixture.psqlAs( "app_runtime", "app-runtime-test-password", `set role authenticated; select set_config('request.jwt.claim.sub', '${owner}', true); select public.delete_chart_subject('${completeId}');`, ); assert.equal(fixture.psql(`select count(*) from public.chart_profiles where id = '${completeId}'`), "0"); assert.equal(fixture.psql(`select count(*) from public.chat_sessions where id = '${sessionId}'`), "0"); assert.equal(fixture.psql(`select count(*) from public.personal_reports where id = '${reportId}'`), "0"); assert.equal(fixture.psql(`select count(*) from public.chat_sessions where id = '${keeper}'`), "1"); assert.equal( fixture.psql(`select count(*) from public.chat_sessions where user_id = '${owner}' and (chart_profile_id is null or chart_profile_id = 'self')`), "1", ); } finally { fixture.stop(); } }); test("actual subject deletion releases queued and running reservations once but never refunds delivered reports", { skip: docker ? false : "docker unavailable", }, () => { const fixture = startPostgresFixture(); try { applyMigrations(fixture); for (const status of ["queued", "running", "ready"] as const) { const owner = randomUUID(), subject = randomUUID(), request = randomUUID(), report = randomUUID(); fixture.psql(`insert into identity.users(id,name,email,email_verified,email_verified_at) values('${owner}','Fictional refund','${owner}@example.invalid',true,now()); update public.profiles set credits=10 where id='${owner}'; insert into public.chart_profiles(id,user_id,role,profile) values('${subject}','${owner}','other','${profileJson()}'::jsonb);`); assert.equal(fixture.psql(`select success::text || '|' || credits::text from public.authorize_usage('${owner}','report.full',null,'${request}',2)`), "true|8"); fixture.psql(`insert into public.personal_reports(id,user_id,chart_profile_id,request_id,request_fingerprint,report_type,status, schema_version,presentation_mode,requested_themes,depth,skill_name,skill_version,skill_source_commit,skill_snapshot_sha256) values('${report}','${owner}','${subject}','${request}','${HASH}','personal_full','generating', 'report_document.v2','default',array['career']::text[],'standard','jyotish-personal-report','1.0.0','${"b".repeat(40)}','${HASH}');`); if (status === "running") fixture.psql(`select * from public.claim_personal_report_job('fictional-worker',60);`); // Ready is already delivered even if its independent billing completion is still pending. if (status === "ready") fixture.psql(`update public.personal_reports set status='ready',completed_at=now(),report_document='{}'::jsonb,calculation_hash='${HASH}',evidence_hash='${HASH}' where id='${report}';`); fixture.psqlAs("app_runtime", "app-runtime-test-password", `set role authenticated; select set_config('request.jwt.claim.sub','${owner}',true); select public.delete_chart_subject('${subject}');`); assert.equal(fixture.psql(`select count(*) from public.personal_reports where id='${report}'`), "0"); assert.equal(fixture.psql(`select count(*) from public.personal_report_jobs where user_id='${owner}' and request_id='${request}'`), "0"); assert.equal(fixture.psql(`select credits from public.profiles where id='${owner}'`), status === "ready" ? "8" : "10"); assert.equal(fixture.psql(`select count(*) from public.credit_transactions where user_id='${owner}' and request_id='${request}' and transaction_type='refund'`), status === "ready" ? "0" : "1"); if (status !== "ready") { assert.equal(fixture.psql(`select status from public.usage_reservations where user_id='${owner}' and request_id='${request}'`), "released"); fixture.psql(`select * from public.release_usage('${owner}','${request}','late_worker');`); assert.equal(fixture.psql(`select credits from public.profiles where id='${owner}'`), "10"); assert.equal(fixture.psql(`select count(*) from public.credit_transactions where user_id='${owner}' and request_id='${request}' and transaction_type='refund'`), "1"); } } } finally { fixture.stop(); } });