0102a973a3
The first-round title guard compared the pre-RPC snapshot, so append_consultation_question had already rewritten the title and the model name never landed. Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
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"]);
|
|
});
|