import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { persistGuardedSessionTitle, type GuardedTitleClient } from "../src/lib/session-title-guard.ts"; const consultRoute = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); function sourceBetween(source: string, start: string, end: string): string { const startIndex = source.indexOf(start); const endIndex = source.indexOf(end, startIndex + start.length); assert.ok(startIndex >= 0, `missing start marker: ${start}`); assert.ok(endIndex > startIndex, `missing end marker: ${end}`); return source.slice(startIndex, endIndex); } function mockClient(options: { rows?: Array<{ id: string }>; error?: unknown; throwOnSelect?: unknown; }): { client: GuardedTitleClient; titleEq: string[] } { const titleEq: string[] = []; const chain = { eq(column: string, value: string) { if (column === "title") titleEq.push(value); return chain; }, select() { if (options.throwOnSelect) return Promise.reject(options.throwOnSelect); return Promise.resolve({ data: options.error ? null : (options.rows ?? []), error: options.error ?? null, }); }, }; return { titleEq, client: { from(table) { assert.equal(table, "chat_sessions"); return { update(values) { assert.equal(typeof values.title, "string"); return chain; }, }; }, }, }; } test("consult generates a title from the pre-RPC snapshot and guards with the post-RPC title", () => { const afterAppend = sourceBetween( consultRoute, "if (!appendedQuestion.success) {", "const usageStartedAt = Date.now();", ); const persist = sourceBetween( consultRoute, "const titleAfterRpc =", "async function runAgenticConsultation", ); assert.match(afterAppend, /const expectedTitle = typeof chatSession\.title === "string" \? chatSession\.title : ""/); assert.match(afterAppend, /\.select\("title"\)[\s\S]*\.eq\("id", sessionId\)[\s\S]*\.eq\("user_id", userId\)[\s\S]*\.maybeSingle\(\)/); assert.match( persist, /shouldGenerateSessionTitle\(\{\s*title: expectedTitle,/, ); assert.match(persist, /expectedTitle: titleAfterRpc/); assert.match(persist, /persistGuardedSessionTitle/); assert.match(persist, /return \{ type: "session\.title" as const, title \}/); assert.doesNotMatch(persist, /\.eq\("title", expectedTitle\)/); }); test("the title guard writes when the current title still matches the post-RPC value", async () => { const { client, titleEq } = mockClient({ rows: [{ id: "session-1" }] }); const warnings: string[] = []; const result = await persistGuardedSessionTitle({ client, sessionId: "session-1", userId: "user-1", expectedTitle: "半年内换工作时机…", title: "换工作窗口", warn: (message) => warnings.push(message), }); assert.equal(result, "updated"); assert.deepEqual(titleEq, ["半年内换工作时机…"]); assert.deepEqual(warnings, []); }); test("the title guard misses and warns when the user renamed the session", async () => { const { client, titleEq } = mockClient({ rows: [] }); const warnings: string[] = []; const result = await persistGuardedSessionTitle({ client, sessionId: "session-1", userId: "user-1", expectedTitle: "半年内换工作时机…", title: "换工作窗口", warn: (message) => warnings.push(message), }); assert.equal(result, "missed"); assert.deepEqual(titleEq, ["半年内换工作时机…"]); assert.deepEqual(warnings, ["session title guard missed"]); });