import assert from "node:assert/strict"; 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 { closeLocalPostgresDataPools, createLocalPostgresDataClient, } from "../src/lib/db/local-postgres-client-core.ts"; import { startPostgresFixture, type PostgresFixture } from "./helpers/postgres-fixture.ts"; 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 schemaPassword = "schema-owner-test-password"; const ownerSessionA = "11111111-1111-4111-8111-1111111114a1"; const ownerSessionB = "11111111-1111-4111-8111-1111111114a2"; const ownerSessionC = "11111111-1111-4111-8111-1111111114a3"; const otherSession = "22222222-2222-4222-8222-2222222224a4"; const ownerProfileP = "33333333-3333-4333-8333-3333333334a5"; const ownerProfileQ = "33333333-3333-4333-8333-3333333334a6"; const otherProfile = "44444444-4444-4444-8444-4444444444a7"; function windowsMigrationDirectory(): string { const directory = mkdtempSync(join(tmpdir(), "jyotisha-order-migrations-")); const seen = new Map(); 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("20261026120000_sidebar_archive_order_batch.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 rpcError(error: unknown): string { if (!error || typeof error !== "object") return ""; const message = (error as { message?: unknown }).message; return typeof message === "string" ? message : ""; } test("order batch rereads the caller's rows and leaves the group unchanged when one id is foreign", { skip: docker ? false : "docker unavailable", }, async () => { const fixture = startPostgresFixture(); try { applyMigrations(fixture); fixture.psql(` insert into identity.users (name, email, email_verified, email_verified_at) values ('Fictional Order', 'order-owner@example.invalid', true, now()), ('Fictional Other', 'order-other@example.invalid', true, now()); `); const owner = fixture.psql("select id from identity.users where email = 'order-owner@example.invalid'"); const other = fixture.psql("select id from identity.users where email = 'order-other@example.invalid'"); fixture.psql(` insert into public.chat_sessions (id, user_id, title, theme, model_id, messages, session_type, updated_at) values ('${ownerSessionA}', '${owner}', 'A', 'general', 'test-model', '[]', 'consultation', now()), ('${ownerSessionB}', '${owner}', 'B', 'general', 'test-model', '[]', 'consultation', now()), ('${ownerSessionC}', '${owner}', 'C', 'general', 'test-model', '[]', 'consultation', now()), ('${otherSession}', '${other}', 'D', 'general', 'test-model', '[]', 'consultation', now()); update public.chat_sessions set sidebar_order = 7 where id = '${otherSession}'; insert into public.chart_profiles (id, user_id, role, profile) values ('${ownerProfileP}', '${owner}', 'other', '{"name":"虚构甲","chartRelationship":"friend"}'::jsonb), ('${ownerProfileQ}', '${owner}', 'other', '{"name":"虚构乙","chartRelationship":"friend"}'::jsonb), ('${otherProfile}', '${other}', 'other', '{"name":"虚构丙","chartRelationship":"friend"}'::jsonb); update public.chart_profiles set archive_order = 7 where id = '${otherProfile}'; `); const sessionOrder = (id: string) => fixture.psql( `select coalesce(sidebar_order::text, 'null') || '|' || updated_at::text from public.chat_sessions where id = '${id}'`, ); const profileOrder = (id: string) => fixture.psql( `select coalesce(archive_order::text, 'null') || '|' || updated_at::text from public.chart_profiles where id = '${id}'`, ); const beforeA = sessionOrder(ownerSessionA); const beforeC = sessionOrder(ownerSessionC); const beforeOtherSession = sessionOrder(otherSession); const beforeP = profileOrder(ownerProfileP); const beforeOtherProfile = profileOrder(otherProfile); const client = createLocalPostgresDataClient( fixture.connectionUrl("app_runtime", "app-runtime-test-password"), { id: owner, email: "order-owner@example.invalid" }, "authenticated", ); const saved = await client.rpc("set_chat_session_sidebar_order", { p_ids: [ownerSessionB, ownerSessionA], p_orders: [10, 20], }); assert.equal(saved.error, null, rpcError(saved.error)); assert.equal(sessionOrder(ownerSessionB).startsWith("10|"), true); assert.equal(sessionOrder(ownerSessionA).startsWith("20|"), true); assert.equal(sessionOrder(ownerSessionA).slice(sessionOrder(ownerSessionA).indexOf("|")), beforeA.slice(beforeA.indexOf("|"))); assert.equal(sessionOrder(ownerSessionC), beforeC); assert.equal(sessionOrder(otherSession), beforeOtherSession); const rejected = await client.rpc("set_chat_session_sidebar_order", { p_ids: [ownerSessionA, ownerSessionB, otherSession], p_orders: [30, 40, 50], }); assert.match(rpcError(rejected.error), /order_row_missing/); assert.equal(sessionOrder(ownerSessionA).startsWith("20|"), true); assert.equal(sessionOrder(ownerSessionB).startsWith("10|"), true); assert.equal(sessionOrder(ownerSessionC), beforeC); assert.equal(sessionOrder(otherSession), beforeOtherSession); const savedProfiles = await client.rpc("set_chart_profile_archive_order", { p_ids: [ownerProfileQ, ownerProfileP], p_orders: [10, 20], }); assert.equal(savedProfiles.error, null, rpcError(savedProfiles.error)); assert.equal(profileOrder(ownerProfileQ).startsWith("10|"), true); assert.equal(profileOrder(ownerProfileP).startsWith("20|"), true); assert.equal(profileOrder(ownerProfileP).slice(profileOrder(ownerProfileP).indexOf("|")), beforeP.slice(beforeP.indexOf("|"))); assert.equal(profileOrder(otherProfile), beforeOtherProfile); const rejectedProfiles = await client.rpc("set_chart_profile_archive_order", { p_ids: [ownerProfileP, otherProfile], p_orders: [30, 40], }); assert.match(rpcError(rejectedProfiles.error), /order_row_missing/); assert.equal(profileOrder(ownerProfileP).startsWith("20|"), true); assert.equal(profileOrder(ownerProfileQ).startsWith("10|"), true); assert.equal(profileOrder(otherProfile), beforeOtherProfile); } finally { await closeLocalPostgresDataPools(); fixture.stop(); } });