Files
Jyotisha/frontend/tests/database-order-batch.test.ts
T
jesse-uxandGrok (agent loop) c6c69aa04b 拖动排序一次写完,新对话仍排在最上面(BUG-1449)。
Co-Authored-By: Grok (agent loop)
2026-10-27 00:03:59 +08:00

167 lines
7.9 KiB
TypeScript

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<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("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();
}
});