import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { spawnSync } from "node:child_process"; import { register } from "node:module"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { closeLocalPostgresDataPool, createLocalPostgresDataClient } from "../src/lib/db/local-postgres-client-core.ts"; import { startPostgresFixture, type PostgresFixture } from "./helpers/postgres-fixture.ts"; // The synastry create path must reach a real reservation and a queued job // through the self-hosted client. The model catalog is stubbed. Fictional rows only. const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url)); const docker = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { stdio: "ignore" }).status === 0; const MODEL_ID = "synastry-fixture-model"; function applyMigrations(fixture: PostgresFixture): void { const result = spawnSync(process.execPath, [runner], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password"), }, }); assert.equal(result.status, 0, result.stderr || result.stdout); } test("createSynastryFullReport reserves synastry.full and enqueues a job on the real client", { skip: docker ? false : "docker unavailable", }, async () => { await register(new URL("./support/stub-server-only.mjs", import.meta.url).href, import.meta.url); const fixture = startPostgresFixture(); const serviceUrl = fixture.connectionUrl("service_runtime", "service-runtime-test-password"); const appUrl = fixture.connectionUrl("app_runtime", "app-runtime-test-password"); const adminUrl = fixture.connectionUrl("admin_runtime", "admin-runtime-test-password"); const previousAuth = process.env.AUTH_PROVIDER; const previousAdminUrl = process.env.ADMIN_DATABASE_URL; process.env.AUTH_PROVIDER = "self-hosted"; process.env.ADMIN_DATABASE_URL = adminUrl; try { applyMigrations(fixture); const userId = randomUUID(); const personId = randomUUID(); fixture.psql(` insert into identity.users (id, name, email, email_verified, email_verified_at) values ('${userId}', '虚构甲', '${userId}@example.invalid', true, now()); update public.profiles set name = '虚构甲', credits = 100, birth_date = '1991-04-07', reported_birth_time = '09:15', birth_time_status = 'reported', birth_time_source = 'family_exact', latitude = 31.2, longitude = 121.4, timezone_offset = 8, timezone_id = 'Asia/Shanghai', ayanamsa = 'lahiri', birth_place_label = '虚构市' where id = '${userId}'; insert into public.chart_profiles ( id, user_id, role, profile, updated_at, name, birth_date, reported_birth_time, birth_time_source, latitude, longitude, timezone_id, timezone_offset, birth_place_label, ayanamsa, birth_time_status ) values ( '${personId}', '${userId}', 'other', '{"name":"虚构乙"}'::jsonb, now(), '虚构乙', '1992-05-06', '10:20', 'family_exact', 22.3, 114.1, 'Asia/Shanghai', 8, '虚构港', 'lahiri', 'reported' ); with provider as ( insert into public.model_providers (code, name, provider_type, encrypted_api_key, enabled) values ('synastry-fixture', 'Synastry Fixture', 'openai', 'test-ciphertext', true) returning id ), config as ( insert into public.model_configs (model_id) values ('${MODEL_ID}') returning id ) insert into public.model_config_versions ( config_id, version, provider_id, label, provider_model, enabled, is_default, status, published_at ) select config.id, 1, provider.id, 'Fixture', 'gpt-test', true, true, 'published', now() from config cross join provider; `); const catalog = await import("../src/lib/model-catalog.ts"); catalog.setModelCatalogIoForTests({ queryAdminRows: (async () => [{ model_id: MODEL_ID, version: 1, label: "Fixture", description: "", provider_model: "gpt-test", credit_cost: 1, is_default: true, provider_code: "synastry-fixture", provider_type: "openai" as const, base_url: null, encrypted_api_key: "test-ciphertext", input_cost: 0, output_cost: 0, context_window: null, }]) as typeof catalog.setModelCatalogIoForTests extends (next: infer T) => void ? T extends { queryAdminRows?: infer Q } ? Q : never : never, decryptModelProviderApiKey: () => "sk-test", }); catalog.invalidateLanguageModelCatalog(); const { createSynastryFullReport } = await import("../src/lib/synastry-full-create.ts"); const user = createLocalPostgresDataClient(appUrl, { id: userId, email: `${userId}@example.invalid` }); const admin = createLocalPostgresDataClient(serviceUrl, null, "service_role"); const created = await createSynastryFullReport({ userId, body: { reportType: "synastry_full", subjectA: "self", subjectB: personId }, supabase: user, admin, }); assert.equal(created.status, 202, JSON.stringify(created.body)); const report = created.body.report as { id?: string; status?: string } | undefined; assert.equal(typeof report?.id, "string"); assert.equal(report?.status, "generating"); assert.equal(typeof created.body.jobId, "string"); assert.equal(fixture.psql(`select status from public.personal_reports where id = '${report?.id}'`), "generating"); assert.equal(fixture.psql(`select status from public.personal_report_jobs where user_id = '${userId}'`), "queued"); assert.equal(fixture.psql(`select status from public.usage_reservations where user_id = '${userId}'`), "reserved"); assert.equal(fixture.psql(`select credits from public.profiles where id = '${userId}'`), "81"); catalog.setModelCatalogIoForTests(null); catalog.invalidateLanguageModelCatalog(); } finally { const database = await import("../src/lib/admin/database.ts"); await database.adminDatabasePool().end().catch(() => undefined); if (previousAuth === undefined) delete process.env.AUTH_PROVIDER; else process.env.AUTH_PROVIDER = previousAuth; if (previousAdminUrl === undefined) delete process.env.ADMIN_DATABASE_URL; else process.env.ADMIN_DATABASE_URL = previousAdminUrl; await closeLocalPostgresDataPool(serviceUrl); await closeLocalPostgresDataPool(appUrl); fixture.stop(); } });