import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { ACTIVITY_APPLY_SQL, TITLE_APPLY_SQL, TITLE_MATCH_SQL, } from "../scripts/repair-rectification-session-titles.mjs"; const filename = "20260916020000_rectification_session_title_repair.sql"; const migration = readFileSync( new URL(`../supabase/migrations/${filename}`, import.meta.url), "utf8", ); // Only trailing whitespace differs: the script builds `where ${TITLE_MATCH_SQL}`, // which leaves a space before the newline that no .sql file should carry. const normalize = (value: string) => value.replace(/[ \t]+$/gm, "").trim(); test("repair migration carries the checker's SQL verbatim", () => { for (const [name, sql] of [ ["TITLE_MATCH_SQL", TITLE_MATCH_SQL], ["TITLE_APPLY_SQL", TITLE_APPLY_SQL], ["ACTIVITY_APPLY_SQL", ACTIVITY_APPLY_SQL], ] as const) { assert.ok( normalize(migration).includes(normalize(sql)), `${name} must be copied into ${filename} without being rewritten`, ); } }); test("repair migration keeps the guards that make it safe to re-run", () => { // Asia/Shanghai, not UTC: the wall-clock date is what the user reads (BUG-699). assert.match(migration, /at time zone 'Asia\/Shanghai'/); // Never guess a date when created_at is missing. assert.match(migration, /when created_at is null then '生时校正'/); // Monotonic: updated_at only moves forward (BUG-704). assert.match( migration, /session\.updated_at < coalesce\(turns\.last_turn_at, case_row\.last_activity_at\)/, ); // Only schema_owner may run it, like every other business migration. assert.match(migration, /rectification_session_title_repair_requires_schema_owner/); assert.match(migration, /^begin;[\s\S]*^commit;$/m); }); test("repair migration reports its row counts so staging logs show a number", () => { assert.match(migration, /get diagnostics repaired_titles = row_count;/); assert.match(migration, /get diagnostics refreshed_activity = row_count;/); assert.match(migration, /raise notice '[^']*repaired_titles=%', repaired_titles;/); assert.match(migration, /raise notice '[^']*refreshed_activity=%', refreshed_activity;/); // The counts only reach the log because the migrator forwards notices. const migrator = readFileSync( new URL("../scripts/db-migrate.mjs", import.meta.url), "utf8", ); assert.match(migrator, /client\.on\("notice"/); }); test("repair migration sorts last and is not copied into the identity foundation", () => { assert.ok(filename > "20260916010000_consultation_session_capacity.sql"); assert.match(filename, /^\d{14}_[a-z0-9_]+\.sql$/); assert.equal( existsSync(fileURLToPath(new URL(`../db/migrations/${filename}`, import.meta.url))), false, "business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)", ); }); test("the checker no longer writes; --apply points at the migration", () => { const checker = readFileSync( new URL("../scripts/repair-rectification-session-titles.mjs", import.meta.url), "utf8", ); assert.match(checker, /--apply has been removed/); assert.match(checker, new RegExp(filename.replace(/\./g, "\\."))); // Importing it must not have opened a connection. assert.match(checker, /invokedPath === import\.meta\.url/); });