Files
Jyotisha/frontend/tests/database-chart-subject.test.ts
T
jesse-uxandClaude Code 4d801e53c3
Independent Staging Quality Gate / validate (push) Successful in 16m28s
Independent Staging Quality Gate / publish (push) Successful in 3m33s
fix: integrate people archive, report reader and western chart corrections
Validate on Linux Node 22 and PostgreSQL 17: 3894 frontend tests and 64 database tests pass, with no removed test names or new failures. Preserve static Home, bounded gzip, assertion-change records and manual acceptance gaps.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 20:41:16 +08:00

365 lines
18 KiB
TypeScript

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<string, Buffer>();
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, unknown> = {}): 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(); }
});