Files
Jyotisha/frontend/tests/session-title-persist-contract.test.ts
T
jesse-uxandGrok (agent loop) cd7e0a3493
Independent Staging Quality Gate / validate (push) Successful in 13m51s
Independent Staging Quality Gate / publish (push) Successful in 4m6s
把普通对话的预留、取消、追加、寒暄结算和用量计算收进结算工厂。
完成回答、直达校正、输出审核和开流前加载仍留在路由里,使 diff 停在 1500 行以内。

Co-Authored-By: Grok (agent loop)
2026-10-15 19:12:44 +08:00

113 lines
3.9 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 appended = await consultSettlement.appendQuestion();
// 原因: 追加与写满返回搬进结算工厂,标题仍从这次调用之后读到 usagePayload
"const appended = await consultSettlement.appendQuestion();",
"async function usagePayload(",
);
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"]);
});