import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION, consultationEntrypointSchema, isGeneralDailyFortuneQuestion, isRectificationHandoffQuestion, resolveConsultationQuestion, shouldLoadGeneralDailyPanchanga, } from "../src/lib/consultation-entrypoint.ts"; test("plain consultation questions remain user-authored", () => { // Given: an ordinary question without a product entrypoint. const visibleQuestion = "未来半年适合换工作吗?"; // When: the server resolves the model-facing question. const resolved = resolveConsultationQuestion({ visibleQuestion, entrypoint: undefined, currentDate: "2026-07-19", }); // Then: the server does not rewrite ordinary user input. assert.deepEqual(resolved, { kind: "plain", modelQuestion: visibleQuestion }); }); test("daily entrypoint selects a private server expansion", () => { // Given: the public short label and its closed entrypoint identity. const visibleQuestion = "深入看今日"; // When: the server resolves the request. const resolved = resolveConsultationQuestion({ visibleQuestion, entrypoint: "daily_starlanguage", currentDate: "2026-07-19", }); // Then: routing is explicit and the model receives more than the public label. assert.equal(resolved.kind, "expanded"); assert.notEqual(resolved.modelQuestion, visibleQuestion); }); test("daily entrypoint without a birth minute selects a public-day expansion", () => { const resolved = resolveConsultationQuestion({ visibleQuestion: GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION, entrypoint: "daily_starlanguage", currentDate: "2026-08-15", consultationMode: "general_no_birth_time", }); assert.equal(resolved.kind, "expanded"); assert.match(resolved.modelQuestion, /公共 Panchanga/); assert.match(resolved.modelQuestion, /不包含个人上升点、宫位、大运或本命过境叠加/); assert.doesNotMatch(resolved.modelQuestion, /已校验的星盘资料/); }); test("ordinary-session daily fortune without an entrypoint still uses the public-day expansion", () => { const resolved = resolveConsultationQuestion({ visibleQuestion: GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION, entrypoint: undefined, currentDate: "2026-08-15", consultationMode: "general_no_birth_time", }); assert.equal(isGeneralDailyFortuneQuestion(GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION), true); assert.equal(shouldLoadGeneralDailyPanchanga({ consultationMode: "general_no_birth_time", visibleQuestion: GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION, }), true); assert.equal(shouldLoadGeneralDailyPanchanga({ consultationMode: "general_no_birth_time", visibleQuestion: "未来半年是否适合换工作?", }), false); assert.equal(shouldLoadGeneralDailyPanchanga({ consultationMode: "declared_birth_window", visibleQuestion: GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION, }), true); assert.equal(shouldLoadGeneralDailyPanchanga({ consultationMode: "declared_birth_window", visibleQuestion: "请帮我看事业方向", }), false); assert.equal(resolved.kind, "expanded"); assert.match(resolved.modelQuestion, /公共 Panchanga/); }); test("rectification labels in ordinary sessions are handoffs, not consult questions", () => { assert.equal(isRectificationHandoffQuestion("出生时间校正 (生时校正)"), true); assert.equal(isRectificationHandoffQuestion("出生时间校正 (生时矫正)"), true); assert.equal(isRectificationHandoffQuestion("先完成生时校正"), true); assert.equal(isRectificationHandoffQuestion("生时校正是什么"), false); assert.equal(isRectificationHandoffQuestion("未来半年是否适合换工作?"), false); }); test("birth-time entrypoint selects a private server expansion", () => { // Given: a completed profile starts another rectification from a public label. const visibleQuestion = "再次校正"; // When: the server resolves the request. const resolved = resolveConsultationQuestion({ visibleQuestion, entrypoint: "birth_time_rectification", currentDate: "2026-07-19", }); // Then: the model question is expanded without changing the visible transcript. assert.equal(resolved.kind, "expanded"); assert.notEqual(resolved.modelQuestion, visibleQuestion); }); test("consultation entrypoints form a closed public request enum", () => { assert.equal(consultationEntrypointSchema.safeParse("daily_starlanguage").success, true); assert.equal(consultationEntrypointSchema.safeParse("birth_time_rectification").success, true); assert.equal(consultationEntrypointSchema.safeParse("client_prompt").success, false); }); test("browser source does not own private entrypoint prompts", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); assert.doesNotMatch(source, /function buildDailyStarlanguageQuestion/); assert.doesNotMatch(source, /function buildBirthTimeRectificationQuestion/); assert.doesNotMatch(source, /请结合已校验的星盘资料/); assert.doesNotMatch(source, /请基于已校验的出生资料继续/); }); test("ordinary product drafts keep the public question and clear hidden routing after edits", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); assert.match(source, /dailyStarlanguageQuestion/); assert.match(source, /从今日问起/); assert.match(source, /深入看今日/); assert.match(source, /chooseSuggestedQuestion\(\s*dailyStarlanguageQuestion,\s*"timing",\s*"daily_starlanguage"/); assert.match(source, /consultEntrypoint === "birth_time_rectification"/); assert.match(source, /isRectificationHandoffQuestion\(question\)/); assert.match(source, /openRectificationFromHomepage\(pendingQuestion\)/); assert.doesNotMatch(source, /personalChartAvailable \? "daily_starlanguage" : null/); assert.match(source, /messages: questionAlreadyPresent \? preservedMessages : \[\.\.\.preservedMessages, \{ role: "user", text: question \}\]/); assert.match(source, /body:\s*JSON\.stringify\(\{[\s\S]*?entrypoint:\s*consultEntrypoint \?\? undefined,[\s\S]*?question,/); const requestBody = source.slice(source.indexOf("body: JSON.stringify({"), source.indexOf("history: currentSession.messages", source.indexOf("body: JSON.stringify({"))); assert.match(requestBody, /consultationMode:[\s\S]*?entrypoint: consultEntrypoint \?\? undefined/); assert.doesNotMatch(requestBody, /general_no_birth_time" \|\| consultationRoute\.mode === "declared_birth_window" \? \{\} : \{[\s\S]*?entrypoint/); assert.match(source, /onChange=\{\(event\) => \{[\s\S]*?setDraft\(event\.target\.value\);[\s\S]*?setDraftTheme\(null\);[\s\S]*?setDraftEntrypoint\(null\);/); assert.match(source, /setDraft\(pending\.question\);[\s\S]*?setDraftTheme\(pending\.theme\);[\s\S]*?setDraftEntrypoint\(pending\.entrypoint\);/); }); test("homepage birth-time card opens the V9 Agentic surface via the server case API", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8"); assert.match(source, /function openRectificationCase/); assert.match(source, /openRectificationFromHomepage/); assert.match(source, //); assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/); assert.doesNotMatch(source, /chooseSuggestedQuestion\([\s\S]{0,180}"birth_time_rectification"/); assert.doesNotMatch(source, /draftBirthTimeRectificationQuestion/); }); test("homepage mounts the Agentic surface without invoking retired rectification starters", () => { const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8"); const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8"); assert.match(page, / { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const start = source.indexOf("async function openRectificationCase"); const end = source.indexOf("async function openRectificationFromHomepage", start); const handler = source.slice(start, end); const open = handler.indexOf('/api/rectification/cases/open'); const merge = handler.indexOf('setSessions((current) => [merged,'); const reveal = handler.indexOf('setActiveSessionId(opened.sessionId)'); assert.ok(open >= 0); assert.ok(merge > open); assert.ok(reveal > merge); assert.match(handler, /rectificationOpenInFlight\.current = true;[\s\S]*?finally \{[\s\S]*?rectificationOpenInFlight\.current = false;/); assert.doesNotMatch(handler, /onNarrativeDelta/); assert.match(source, /const rectificationSurfaceOpen = activeRectificationSession\s*&& activeSession\.id === rectificationSessionId/); assert.match(source, /rectificationSurfaceOpen && rectificationCaseId && \([\s\S]*? { const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8"); const start = page.indexOf("async function openRectificationCase"); const end = page.indexOf("async function openRectificationFromHomepage", start); const handler = page.slice(start, end); assert.doesNotMatch(handler, /persistSession\(rectificationSession, "create"\)/); assert.match(handler, /const merged: ChatSession = \{/); assert.match(component, / { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); assert.match(source, /activeSession\?\.sessionType === "birth_time_rectification"/); assert.match(source, /session_type:\s*session\.sessionType/); assert.match(source, /rectification_case_id:\s*session\.rectificationCaseId/); assert.doesNotMatch(source, /这个会话保存了生时校正入口|恢复生时校正<\/button>/); }); test("selecting a rectification session resumes it through the exact-session open API", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const selectSession = source.slice( source.indexOf("function selectSession("), source.indexOf("async function selectSessionModel", source.indexOf("function selectSession(")), ); assert.match(selectSession, /nextSession\?\.sessionType === "birth_time_rectification"/); assert.match(selectSession, /void openRectificationSession\(nextSession\.id\)/); assert.match(source, /resumeRectificationSession\.current\(activeSession\)/); assert.doesNotMatch(source, /RectificationLoadingState|重试恢复/); assert.match(source, / { const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8"); assert.match(page, /openRectificationCase\(\"session\", exactSessionId, null\)/); assert.match(page, /openRectificationCase\(\"homepage\", null, pendingConsultationQuestion\)/); assert.match(page, /openRectificationCase\(\"new\", null, null\)/); assert.doesNotMatch(page, /sourceSession\.sessionType === "birth_time_rectification"/); assert.doesNotMatch(page, /sessions\.find\(\(session\) => session\.sessionType === "birth_time_rectification"\)/); assert.match(component, / { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); // Given: the chips above the composer were removed after measured use stayed negligible. assert.doesNotMatch(source, /composer-suggestions|activeSuggestions|chooseConversationSuggestion/); assert.doesNotMatch(styles, /composer-suggestions/); // Then: nothing in the conversation carries or stores a suggestion list any more. assert.doesNotMatch(source, /suggestions: (?:reply|previewReply|parsed)\.suggestions/); assert.doesNotMatch(source, /readSuggestions/); // And: rectification is still reachable without the chip that used to hand off to it. assert.match(source, /onClick=\{\(\) => void openRectificationFromHomepage\(\)\}/); }); test("rectify-first handoffs stay as Agent context", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8"); assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/); assert.match(chat, /pendingConsultationQuestion\?\.trim\(\)/); assert.match(chat, /之后会回到你原来的问题/); }); test("ordinary consultation uses current birth data without a rectification notice", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const sendStart = source.indexOf("async function send("); const consultCall = source.indexOf('fetch("/api/consult"', sendStart); assert.ok(sendStart >= 0); assert.ok(consultCall > sendStart); assert.match(source, /mode: "general_no_birth_time" as const/); assert.doesNotMatch(source, / { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); assert.match(source, /onPendingChange=\{setRectificationMutationPending\}/); assert.match(source, /disabled=\{productEntrypointsDisabled \|\| rectificationLoading \|\| rectificationMutationPending\}/); assert.doesNotMatch(source, /重试恢复/); assert.doesNotMatch(source, /返回并恢复原问题|返回首页/); }); test("session changes contain no birth-time notice state", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const selectSession = source.slice( source.indexOf("function selectSession("), source.indexOf("async function selectSessionModel", source.indexOf("function selectSession(")), ); assert.doesNotMatch(selectSession, /birthTimeSoftNotice|setBirthTimeSoftNotice/); assert.doesNotMatch(source, /dismissBirthTimeSoftNotice/); }); test("profile and place saves do not auto-start the retired assessment flow", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const normalSave = source.slice(source.indexOf("async function saveProfile"), source.indexOf("async function saveOnboardingName")); const placeSave = source.slice(source.indexOf("async function saveOnboardingPlace"), source.indexOf("function completeGuidedBirthTime")); assert.doesNotMatch(normalSave, /assessSavedBirthTime|requestBirthTimeAssessment/); assert.doesNotMatch(placeSave, /assessSavedBirthTime|requestBirthTimeAssessment/); }); test("consult route expands an optional entrypoint for both Agent and tool input", () => { const source = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); assert.match(source, /entrypoint:\s*consultationEntrypointSchema\.optional\(\)/); assert.match(source, /question:\s*resolvedQuestion\.modelQuestion/); assert.match(source, /请按 skill Level 2 模板回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/); assert.doesNotMatch(source, /JSON\.stringify\(toolInput\)/); assert.match(source, /shouldLoadGeneralDailyPanchanga\(\{/); assert.doesNotMatch(source, /用户明确选择的无出生分钟一般咨询/); assert.match(source, /不要把整轮对话改成「一般知识或生时校正」二选一/); }); test("homepage entrypoints use two whole-card native actions", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const wholeCardActions = source.match(/className="product-entrypoint-hitarea"/g) ?? []; assert.equal(wholeCardActions.length, 2); assert.doesNotMatch(source, /className="daily-starlanguage-heading">[\s\S]{0,180}