167 lines
7.9 KiB
TypeScript
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();
|
|
}
|
|
});
|