Files
Jyotisha/frontend/tests/database-chart-subject.test.ts
T
jesse-uxandCursor a0e84845e9
Independent Staging Quality Gate / validate (push) Successful in 17m28s
Independent Staging Quality Gate / publish (push) Successful in 23m1s
feat(people): raise the chart archive cap to 20 people
The trigger, the shared constants, and the limit sentence now agree, and the archive and person menus scroll instead of growing past the screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 16:33:06 +08:00

386 lines
20 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);
// 原值: 纠正迁移把函数写回「他人 >= 4 拒绝」,并发的第 4、第 5 个他人争最后一格,提交后剩 4 人。
// 新值: 纠正迁移之后再套上 20261017 的函数(他人 >= 19 拒绝)。先补到 18 个他人,并发的第 19 个可提交,第 20 个被拒,最后剩 19。
// 原因: 产品 2026-10-17 把档案上限改为 20 人(含本人)。锁和 role 检查没改,只把争用点挪到新门槛上。旧迁移文件本身仍是 4,不改。
const limit20 = readFileSync(new URL("../supabase/migrations/20261017010000_chart_profiles_limit_20.sql", import.meta.url), "utf8");
fixture.psql(limit20);
for (let index = 0; index < 15; index += 1) {
const payload = profileJson({ name: `虚构并发${index}` });
fixture.psql(`insert into public.chart_profiles(user_id,role,profile) values ('${owner}','other','${payload}'::jsonb);`);
}
assert.equal(fixture.psql(`select count(*) from public.chart_profiles where user_id='${owner}'`), "18");
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 twentieth = 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, "the insert past nineteen others waits for the same owner's transaction lock");
await first.query("commit");
assert.match((await twentieth).error, /subject_limit_reached/);
await second.query("rollback");
assert.equal(fixture.psql(`select count(*) from public.chart_profiles where user_id='${owner}'`), "19");
} 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",
);
// 原值: 前面已有 2 个他人,循环再插入 2 个后,下一个(第 5 个他人)被拒。
// 新值: 循环再插入 17 个,第 19 个他人可存;再插一个(第 20 个他人,连本人共 21 人)被拒,人数停在 19。
// 原因: 产品 2026-10-17 把档案上限改为 20 人(含本人,他人最多 19)。
for (let index = 0; index < 17; index += 1) {
fixture.psql(`
insert into public.chart_profiles (id, user_id, role, profile)
values ('${randomUUID()}', '${owner}', 'other', '${profileJson({ name: `虚构${index}` })}'::jsonb);
`);
}
assert.equal(
fixture.psql(`select count(*) from public.chart_profiles where user_id = '${owner}' and role = 'other'`),
"19",
);
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.equal(
fixture.psql(`select count(*) from public.chart_profiles where user_id = '${owner}' and role = 'other'`),
"19",
);
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(); }
});