对话、星盘、星历、报告进同一 (app) 外壳,列表只拉一次。服务端不再列出空咨询;新建复用已有空会话。新标题改成「生时校正 · M月D日」,侧栏副标题用创建时间。
122 lines
6.6 KiB
TypeScript
122 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import { homeSurface as page } from "./home-surface.ts";
|
||
const listRoute = readFileSync(new URL("../src/app/api/sessions/route.ts", import.meta.url), "utf8");
|
||
const itemRoute = readFileSync(new URL("../src/app/api/sessions/[id]/route.ts", import.meta.url), "utf8");
|
||
const consultRoute = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||
const sql = readFileSync(
|
||
new URL("../supabase/migrations/20260901010000_append_consultation_question.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const sendSource = page.slice(page.indexOf(" async function send("), page.indexOf("\n\n consultationReplay.current"));
|
||
|
||
test("session list GET drops empty consultations and still returns a reusable draft", () => {
|
||
assert.match(listRoute, /\.not\("messages", "eq", \[\]\)/);
|
||
assert.match(listRoute, /\.eq\("session_type", "consultation"\)[\s\S]*\.eq\("messages", \[\]\)/);
|
||
assert.match(listRoute, /return NextResponse\.json\(\{ sessions, nextCursor, draft \}\)/);
|
||
assert.doesNotMatch(itemRoute, /\.not\("messages", "eq", \[\]\)/);
|
||
});
|
||
|
||
test("session list GET omits messages while detail GET returns them", () => {
|
||
// Former list/detail column strings ended at updated_at; pinned and archived_at
|
||
// were added when those flags moved off localStorage.
|
||
// 原值:列表列到 updated_at,pinned,archived_at,不选 created_at
|
||
// 新值:加上 created_at,供侧栏副标题用会话创建时间
|
||
// 原因:任务书 T4 副标题是 M月D日 HH:MM(创建时间,Asia/Shanghai)
|
||
assert.match(
|
||
listRoute,
|
||
/SESSION_LIST_COLUMNS = "id,title,theme,model_id,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,created_at,updated_at,pinned,archived_at"/,
|
||
);
|
||
// 原值:select(SESSION_LIST_COLUMNS) 之后不得出现 messages
|
||
// 新值:WHERE 用 .not("messages", "eq", []),SELECT 列仍不含 messages
|
||
// 原因:T3 空咨询过滤要看 messages,但不把正文带回列表
|
||
assert.doesNotMatch(listRoute, /SESSION_LIST_COLUMNS = "[^"]*messages/);
|
||
assert.doesNotMatch(
|
||
listRoute,
|
||
/SESSION_LIST_COLUMNS = "[^"]*context_summary/,
|
||
);
|
||
assert.match(itemRoute, /export async function GET/);
|
||
assert.match(
|
||
itemRoute,
|
||
/sessionSelect = "id,title,theme,model_id,messages,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,created_at,updated_at,pinned,archived_at"/,
|
||
);
|
||
assert.match(page, /async function fetchSessionDetail\(/);
|
||
assert.match(page, /async function ensureSessionMessages\(/);
|
||
assert.match(page, /session-messages-loading/);
|
||
assert.match(page, /messagesHydrated: messagesPresent/);
|
||
});
|
||
|
||
test("consult POST on a rectification session is 409, a missing id is 404", () => {
|
||
// 原值:!chatSession || session_type !== consultation 一律 404「咨询会话不存在」
|
||
// 新值:查不到 404;类型不对 409 session_not_consultation
|
||
// 原因:BUG-925 前端无法区分不存在与类型不对
|
||
assert.match(consultRoute, /if \(!chatSession\) \{[\s\S]*status: 404/);
|
||
assert.match(consultRoute, /code: "session_not_consultation"/);
|
||
assert.match(consultRoute, /status: 409/);
|
||
assert.doesNotMatch(
|
||
consultRoute,
|
||
/if \(!chatSession \|\| chatSession\.session_type !== "consultation"\)/,
|
||
);
|
||
});
|
||
|
||
test("consult reads stored history and never applies the client history field", () => {
|
||
assert.match(consultRoute, /history: z\s*\.array\([\s\S]*?\)\s*\.max\(20\)\s*\.optional\(\)\s*\.default\(\[\]\)/);
|
||
assert.match(consultRoute, /const storedHistory = historyWindow\.tail/);
|
||
assert.match(consultRoute, /const history = storedHistory/);
|
||
assert.doesNotMatch(consultRoute, /parsed\.data\.history/);
|
||
assert.match(sendSource, /history: currentSession\.messages\.slice\(-12\)/);
|
||
});
|
||
|
||
test("consult appends the user question after reserve and returns session_full", () => {
|
||
assert.match(consultRoute, /accounting\.rpc\("append_consultation_question"/);
|
||
assert.match(consultRoute, /error_code === "session_full"/);
|
||
assert.match(consultRoute, /code: "session_full"/);
|
||
assert.match(consultRoute, /这段对话已写满,开个新对话继续吧/);
|
||
assert.match(sql, /create or replace function public\.append_consultation_question/);
|
||
assert.match(sql, /pg_advisory_xact_lock/);
|
||
assert.match(sql, /session_type = 'consultation'/);
|
||
assert.match(sql, /'session_full'::text/);
|
||
assert.match(sql, /grant execute on function public\.append_consultation_question/);
|
||
assert.match(sendSource, /caught\.code === "session_full"/);
|
||
assert.match(sendSource, /label: "开新对话"/);
|
||
assert.match(sendSource, /continueInNewChat\(\{ question: originalQuestion, theme \}\)/);
|
||
assert.match(page, /continuedFromSessionId: sourceSessionId/);
|
||
assert.doesNotMatch(page, /接着上次|继续上次聊|继承会话摘要|从上次对话继续/);
|
||
});
|
||
|
||
test("PATCH compatibility accepts and ignores a legacy messages write", () => {
|
||
assert.match(itemRoute, /logIgnoredSessionMessages\(id, user\.id, ignoredMessageCount\(payload\)\)/);
|
||
assert.match(itemRoute, /if \(!values && payloadHasMessages\(payload\)\) \{/);
|
||
assert.match(itemRoute, /return NextResponse\.json\(\{ ok: true \}\)/);
|
||
assert.doesNotMatch(itemRoute, /\.update\(\{[\s\S]*messages:/);
|
||
});
|
||
|
||
test("pin and archive flags are session metadata, not localStorage", () => {
|
||
const pinSql = readFileSync(
|
||
new URL("../supabase/migrations/20260901020000_chat_session_pin_archive.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(pinSql, /add column if not exists pinned boolean not null default false/);
|
||
assert.match(pinSql, /add column if not exists archived_at timestamptz null/);
|
||
assert.match(pinSql, /grant update \(pinned, archived_at\)/);
|
||
assert.match(page, /function applyLegacySessionControls\(/);
|
||
assert.match(page, /writeChatSession\(sessionId, \{ pinned: nextPinned \}, "update"\)/);
|
||
assert.match(page, /writeChatSession\(sessionId, \{ archived_at: nextArchivedAt \}, "update"\)/);
|
||
assert.match(page, /discardLegacyCloudMirrorKeys\(/);
|
||
assert.doesNotMatch(page, /setPinnedSessionIds/);
|
||
assert.doesNotMatch(page, /localStorage\.setItem\(`\$\{prefix\}pinned`/);
|
||
assert.doesNotMatch(page, /writeSynastryHistory\(/);
|
||
});
|
||
|
||
test("session list GET pages by cursor and returns pinned on the first page", () => {
|
||
assert.match(listRoute, /clampSessionLimit/);
|
||
assert.match(listRoute, /parseSessionCursor/);
|
||
assert.match(listRoute, /eq\("pinned", false\)/);
|
||
assert.match(listRoute, /eq\("pinned", true\)/);
|
||
assert.match(listRoute, /isArchivedSessionQuery/);
|
||
assert.match(listRoute, /nextCursor/);
|
||
assert.match(listRoute, /limit \+ 1/);
|
||
});
|