import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { closeLocalPostgresDataPools, createLocalPostgresDataClient, } from "../src/lib/db/local-postgres-client-core.ts"; import { cloudListIncludesSession, excludeEmptyConsultations, } from "../src/lib/session-list-filter.ts"; import { startPostgresFixture } from "./helpers/postgres-fixture.ts"; const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url)); const retireSql = readFileSync( new URL("../supabase/migrations/20260921010000_retire_chat_session_archive.sql", import.meta.url), "utf8", ); const docker = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { stdio: "ignore" }).status === 0; const emptyConsultation = "11111111-1111-4111-8111-111111111111"; const emptyRectification = "22222222-2222-4222-8222-222222222222"; const filledConsultation = "33333333-3333-4333-8333-333333333333"; const archivedRectification = "44444444-4444-4444-8444-444444444444"; const archivedEmptyConsultation = "55555555-5555-4555-8555-555555555555"; test("session list query keeps empty rectification rows and drops empty consultations", { skip: docker ? false : "docker unavailable", }, async () => { const fixture = startPostgresFixture(); try { const migration = spawnSync(process.execPath, [runner], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password"), }, }); assert.equal(migration.status, 0, migration.stderr); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", `insert into identity.users(name,email,email_verified,email_verified_at) values ('Fictional List','list-visibility@example.com',true,now());`, ); const userId = fixture.psql("select id from identity.users where email='list-visibility@example.com'"); fixture.psql(` insert into public.chat_sessions (id, user_id, title, theme, model_id, messages, session_type, pinned, archived_at, updated_at) values ('${emptyConsultation}', '${userId}', 'Empty consult', 'general', 'test-model', '[]', 'consultation', false, null, now()), ('${emptyRectification}', '${userId}', 'Empty rectification', 'general', 'test-model', '[]', 'birth_time_rectification', false, null, now()), ('${filledConsultation}', '${userId}', 'Filled consult', 'general', 'test-model', '[{"role":"user","text":"问一句"}]', 'consultation', false, null, now()), ('${archivedRectification}', '${userId}', 'Archived rectification', 'general', 'test-model', '[]', 'birth_time_rectification', false, '2026-09-08T00:00:00Z', now()), ('${archivedEmptyConsultation}', '${userId}', 'Archived empty consult', 'general', 'test-model', '[]', 'consultation', false, '2026-09-08T00:00:00Z', now()); `); const local = createLocalPostgresDataClient( fixture.connectionUrl("app_runtime", "app-runtime-test-password"), { id: userId, email: "list-visibility@example.com" }, ); const live = await excludeEmptyConsultations( local.from("chat_sessions").select("id").eq("user_id", userId).eq("pinned", false).is("archived_at", null), ); assert.equal(live.error, null, live.error?.message); const liveIds = (live.data as { id: string }[]).map((row) => row.id).sort(); assert.deepEqual(liveIds, [emptyRectification, filledConsultation].sort()); assert.equal(liveIds.includes(archivedRectification), false); assert.equal(liveIds.includes(archivedEmptyConsultation), false); const rows = [ { id: emptyConsultation, sessionType: "consultation" as const, messagesEmpty: true, archived: false }, { id: emptyRectification, sessionType: "birth_time_rectification" as const, messagesEmpty: true, archived: false }, { id: filledConsultation, sessionType: "consultation" as const, messagesEmpty: false, archived: false }, { id: archivedRectification, sessionType: "birth_time_rectification" as const, messagesEmpty: true, archived: true }, { id: archivedEmptyConsultation, sessionType: "consultation" as const, messagesEmpty: true, archived: true }, ]; assert.deepEqual( rows.filter((row) => cloudListIncludesSession(row)).map((row) => row.id).sort(), liveIds, ); } finally { await closeLocalPostgresDataPools(); fixture.stop(); } }); test("retire archive migration clears archived_at without bumping updated_at", { skip: docker ? false : "docker unavailable", }, async () => { const fixture = startPostgresFixture(); try { const migration = spawnSync(process.execPath, [runner], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password"), }, }); assert.equal(migration.status, 0, migration.stderr); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", `insert into identity.users(name,email,email_verified,email_verified_at) values ('Fictional Unarchive','unarchive@example.com',true,now());`, ); const userId = fixture.psql("select id from identity.users where email='unarchive@example.com'"); fixture.psql(` insert into public.chat_sessions (id, user_id, title, theme, model_id, messages, session_type, pinned, archived_at, updated_at) values ( '${archivedRectification}', '${userId}', 'Archived rectification', 'general', 'test-model', '[]', 'birth_time_rectification', true, '2026-09-08T00:00:00Z', '2026-09-10T08:00:00Z' ); `); const before = fixture.psql( `select pinned::text || '|' || jsonb_array_length(messages)::text || '|' || updated_at::text from public.chat_sessions where id = '${archivedRectification}'`, ); assert.match(retireSql, /set archived_at = null/); assert.doesNotMatch(retireSql, /updated_at\s*=/); const apply = retireSql.match(/update public\.chat_sessions\s+set archived_at = null\s+where archived_at is not null;/); assert.ok(apply?.[0], "retire migration must contain the unarchive update"); fixture.psqlAs("schema_owner", "schema-owner-test-password", apply[0]); assert.equal(fixture.psql("select count(*) from public.chat_sessions where archived_at is not null"), "0"); const after = fixture.psql( `select pinned::text || '|' || jsonb_array_length(messages)::text || '|' || updated_at::text from public.chat_sessions where id = '${archivedRectification}'`, ); assert.equal(after, before); fixture.psqlAs("schema_owner", "schema-owner-test-password", apply[0]); assert.equal(fixture.psql("select count(*) from public.chat_sessions where archived_at is not null"), "0"); } finally { fixture.stop(); } });