Files
Jyotisha/frontend/tests/chat-session-authority.test.ts
T
jesse-ux e4e73f56c0
Independent Staging Quality Gate / publish (push) Canceled after 0s
Independent Staging Quality Gate / validate (push) Canceled after 9m33s
fix(web): 四个页面共用一份会话列表,空会话不入列
对话、星盘、星历、报告进同一 (app) 外壳,列表只拉一次。服务端不再列出空咨询;新建复用已有空会话。新标题改成「生时校正 · M月D日」,侧栏副标题用创建时间。
2026-09-17 21:27:40 +08:00

122 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/);
});