The chat stays bound to that section's saved calculation, checks each chart fact before answering, and bills like an ordinary turn. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
6.2 KiB
TypeScript
132 lines
6.2 KiB
TypeScript
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));
|
||
const userId = "83000000-0000-4000-8000-000000000083";
|
||
const otherUserId = "83000000-0000-4000-8000-000000000183";
|
||
const personId = "83000000-0000-4000-8000-000000000283";
|
||
const partnerId = "83000000-0000-4000-8000-000000000383";
|
||
const reportId = "83000000-0000-4000-8000-000000000483";
|
||
const otherReportId = "83000000-0000-4000-8000-000000000583";
|
||
const sessionId = "83000000-0000-4000-8000-000000000683";
|
||
const requestId = "83000000-0000-4000-8000-000000000783";
|
||
const otherRequestId = "83000000-0000-4000-8000-000000000883";
|
||
const fingerprint = "a".repeat(64);
|
||
const skillHash = "d".repeat(64);
|
||
|
||
function dockerAvailable(): boolean {
|
||
return spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { encoding: "utf8", stdio: "ignore" }).status === 0;
|
||
}
|
||
|
||
function errorText(error: unknown): string {
|
||
if (!error || typeof error !== "object") return String(error);
|
||
const stderr = "stderr" in error ? String((error as { stderr?: unknown }).stderr ?? "") : "";
|
||
const stdout = "stdout" in error ? String((error as { stdout?: unknown }).stdout ?? "") : "";
|
||
return `${stderr}\n${stdout}\n${String(error)}`;
|
||
}
|
||
|
||
test("synastry chat binds an owned section, rejects another report, and stays readable after delete", {
|
||
skip: dockerAvailable() ? false : "docker unavailable on this host",
|
||
}, () => {
|
||
const fixture = startPostgresFixture();
|
||
try {
|
||
const migration = spawnSync(process.execPath, [runnerPath], {
|
||
encoding: "utf8",
|
||
env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password") },
|
||
});
|
||
assert.equal(migration.status, 0, `${migration.stdout}${migration.stderr}`);
|
||
|
||
fixture.psql(`
|
||
insert into identity.users (id, name, email, email_verified, email_verified_at)
|
||
values
|
||
('${userId}', 'Fictional Synastry Chat', '${userId}@example.invalid', true, now()),
|
||
('${otherUserId}', 'Fictional Other', '${otherUserId}@example.invalid', true, now());
|
||
update public.profiles set credits = 10 where id = '${userId}';
|
||
insert into public.chart_profiles (id, user_id, role, profile)
|
||
values
|
||
('${personId}', '${userId}', 'other', '{}'::jsonb),
|
||
('${partnerId}', '${userId}', 'other', '{}'::jsonb);
|
||
`);
|
||
const insertReport = (id: string, owner: string, request: string, person: string, partner: string) => `
|
||
insert into public.personal_reports (
|
||
id, user_id, chart_profile_id, partner_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,
|
||
subject_low, subject_high, fingerprint_low, fingerprint_high, synastry_rules_version
|
||
) values (
|
||
'${id}', '${owner}', '${person}', '${partner}', '${request}', '${fingerprint}',
|
||
'synastry_full', 'generating', 'synastry_report.v1', 'default', array[]::text[], 'standard',
|
||
'jyotish-full-synastry-reading', '1.0.0', '${"e".repeat(40)}', '${skillHash}',
|
||
'${person}', '${partner}', '${fingerprint}', '${"b".repeat(64)}', 'bde5d615:marriage_match_product_v1'
|
||
);
|
||
`;
|
||
fixture.psql(insertReport(reportId, userId, requestId, personId, partnerId));
|
||
fixture.psql(`
|
||
update public.personal_reports
|
||
set status = 'ready', completed_at = now(), report_document = '{"names":{"a":"甲","b":"乙"}}'::jsonb,
|
||
calculation_hash = '${skillHash}', evidence_hash = '${skillHash}'
|
||
where id = '${reportId}';
|
||
update public.personal_report_jobs
|
||
set status = 'cancelled', finished_at = now()
|
||
where user_id = '${userId}' and status = 'queued';
|
||
`);
|
||
fixture.psql(insertReport(otherReportId, otherUserId, otherRequestId, personId, partnerId));
|
||
|
||
fixture.psql(`
|
||
insert into public.chat_sessions (
|
||
id, user_id, title, theme, messages, session_type,
|
||
synastry_report_id, synastry_section, synastry_partner_subject_id,
|
||
chart_profile_id, chart_profile_name, chart_profile_role
|
||
) values (
|
||
'${sessionId}', '${userId}', '甲 × 乙 · 合盘追问', 'marriage', '[]'::jsonb, 'synastry_report',
|
||
'${reportId}', 'climate', '${partnerId}',
|
||
'${personId}', '甲 × 乙', 'other'
|
||
);
|
||
`);
|
||
|
||
assert.throws(() => fixture.psql(`
|
||
insert into public.chat_sessions (
|
||
id, user_id, title, theme, messages, session_type,
|
||
synastry_report_id, synastry_section, synastry_partner_subject_id
|
||
) values (
|
||
'83000000-0000-4000-8000-000000000983', '${userId}', '越权', 'marriage', '[]'::jsonb, 'synastry_report',
|
||
'${otherReportId}', 'climate', '${partnerId}'
|
||
);
|
||
`), (error: unknown) => /synastry_report_not_owned/.test(errorText(error)));
|
||
|
||
assert.throws(() => fixture.psql(`
|
||
insert into public.chat_sessions (
|
||
id, user_id, title, theme, messages, session_type,
|
||
synastry_report_id, synastry_section, synastry_partner_subject_id
|
||
) values (
|
||
'83000000-0000-4000-8000-000000001083', '${userId}', '非法节', 'marriage', '[]'::jsonb, 'synastry_report',
|
||
'${reportId}', 'practices', '${partnerId}'
|
||
);
|
||
`), (error: unknown) => /synastry_section_invalid/.test(errorText(error)));
|
||
|
||
const reserved = fixture.psql(`
|
||
select reason from public.reserve_consultation_usage(
|
||
'${userId}', 'chat-83', '${sessionId}', 'model-a', 1
|
||
);
|
||
`);
|
||
assert.equal(reserved.includes("session_missing"), false);
|
||
|
||
fixture.psql(`delete from public.personal_reports where id = '${reportId}';`);
|
||
assert.equal(fixture.psql(`
|
||
select (synastry_report_id is null)::text || '|' || synastry_section || '|' || session_type
|
||
from public.chat_sessions where id = '${sessionId}';
|
||
`), "true|climate|synastry_report");
|
||
assert.equal(fixture.psql(`
|
||
select reason from public.reserve_consultation_usage(
|
||
'${userId}', 'chat-83-after-delete', '${sessionId}', 'model-a', 1
|
||
);
|
||
`), "session_missing");
|
||
} finally {
|
||
fixture.stop();
|
||
}
|
||
});
|