Files
Jyotisha/frontend/tests/consultation-entrypoint.test.ts
T
Jesse_ChenandClaude Fable 5.1 84b293fb47
Independent Staging Quality Gate / validate (push) Failing after 7m5s
Independent Staging Quality Gate / publish (push) Skipped
feat(consult): 对话口气改成反差与扮演象的形状
产品判定现有人设(懂行、可靠、说人话的占星师朋友)出来的是顾问报告。
人设改成把人当一个人认真对待、行动力很强、嘴有点毒但靠谱的同事:直接、
有立场、带一点锋利,毒只对处境不对人且每句锋利都要有盘上的证据。

开场从「一句结论 + 2–3 条短要点 + 一句下一步」换成固定形状,三种模式共用:
反差(表面 A 底下 B,命名成一个格局)→ 谁在推、谁在修(大运主星在推,
行运只负责把结果修得体面)→ 别去应 X 的象,去扮演 Y 的象 → 最多三条短行动
(破折号短句,各 ≤ 20 字)。仍无标题、总长 ≤ 400 字。术语当场用引号里的
白话套住。申报时段与无出生分钟两条降级路线形状照给,只把「谁在推谁在修」
换成窗口内稳定层或公开日历,不编月份。

新增希望纪律:盘上有转机且 answer_policy 允许精确应期时说到月份;没有就说
这段时间是拿来干什么的、可以扮演哪个象。禁「一切都会好 / 相信自己 / 加油 /
你值得更好的 / 宇宙自有安排」。

零业务逻辑改动,Skill 版本不变。tsc 0 错、lint 0 error(118 warning 不变)、
npm test 3468→3471 条且 36 条失败与基线 ff0427cf 逐条相同、/ 仍 Static、
首屏 gzip 两侧字节相同。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-17 16:18:07 +00:00

475 lines
26 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 {
GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION,
consultationEntrypointSchema,
isGeneralDailyFortuneQuestion,
isRectificationHandoffQuestion,
pinsConsultationDomains,
resolveConsultationQuestion,
shouldLoadGeneralDailyPanchanga,
} from "../src/lib/consultation-entrypoint.ts";
import { homeSurface } from "./home-surface.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("homepage and rectification entrypoints pin the server-selected domain", () => {
assert.equal(pinsConsultationDomains("daily_starlanguage"), true);
assert.equal(pinsConsultationDomains("birth_time_rectification"), true);
assert.equal(pinsConsultationDomains("guided_topic"), true);
assert.equal(pinsConsultationDomains(undefined), false);
});
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("guided-topic entrypoint keeps the visible question and pins the theme", () => {
const visibleQuestion = "请帮我看看我家庭关系的整体模式和特点";
const resolved = resolveConsultationQuestion({
visibleQuestion,
entrypoint: "guided_topic",
currentDate: "2026-09-09",
});
assert.deepEqual(resolved, { kind: "plain", modelQuestion: visibleQuestion });
assert.equal(pinsConsultationDomains("guided_topic"), true);
});
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("guided_topic").success, true);
assert.equal(consultationEntrypointSchema.safeParse("client_prompt").success, false);
});
test("browser source does not own private entrypoint prompts", () => {
const source = homeSurface;
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 = homeSurface;
assert.match(source, /dailyStarlanguageQuestion/);
assert.match(source, /从今日问起/);
assert.match(source, /深入看今日/);
assert.match(source, /startSuggestedConsultation\(\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 = homeSurface;
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, /<ConversationalBirthTimeRectification/);
assert.match(component, /<RectificationAgenticChat \{\.\.\.props\} \/>/);
// 原值:pendingConsultationQuestion={rectificationPendingQuestion} 写在 page.tsx JSX
// 新值:pendingConsultationQuestion: rectificationPendingQuestion 写在 hook 的 panel
// 原因:校正面子树状态收到 panel,Home 不再逐个传
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 = homeSurface;
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, /<ConversationalBirthTimeRectification/);
assert.match(component, /<RectificationAgenticChat/);
assert.match(chat, /void send\("opening", ""\)/);
});
test("homepage opens through the server Case API and merges the returned session", () => {
const source = homeSurface;
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/);
// 原值:同一守卫内 JSX 写 pendingConsultationQuestion={rectificationPendingQuestion}
// 新值:同一守卫内 JSX 写 panel={rectificationPanel}pending 字段在 hook panel
// 原因:校正面子树状态收到 panel
assert.match(source, /rectificationSurfaceOpen && rectificationCaseId && \([\s\S]*?<ConversationalBirthTimeRectification[\s\S]*?panel=\{rectificationPanel\}/);
});
test("the page never creates the session shell locally; the server owns session creation", () => {
const page = homeSurface;
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, /<RectificationAgenticChat/);
});
test("rectification cards render only inside the active rectification session", () => {
const source = homeSurface;
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 = homeSurface;
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, /<ConversationalBirthTimeRectification/);
});
test("homepage creation and sidebar selection resolve through distinct server intents", () => {
const page = homeSurface;
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, /<RectificationAgenticChat/);
});
test("an answered conversation offers no suggested follow-up questions", () => {
const source = homeSurface;
const transcript = readFileSync(new URL("../src/components/chat-transcript.tsx", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
// Given: the chips above the composer stay gone. Grounded continuations sit under
// the latest answer and send immediately instead of filling the composer.
assert.doesNotMatch(source, /composer-suggestions|activeSuggestions|chooseConversationSuggestion/);
assert.doesNotMatch(styles, /composer-suggestions/);
assert.match(transcript, /ConversationFollowUps/);
assert.match(transcript, /deriveConsultationFollowUps/);
assert.match(styles, /\.conversation-follow-ups/);
// 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 = homeSurface;
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
// 原值:pendingConsultationQuestion={rectificationPendingQuestion} 写在 page.tsx JSX
// 新值:pendingConsultationQuestion: rectificationPendingQuestion 写在 hook 的 panel
// 原因:校正面子树状态收到 panel,Home 不再逐个传
assert.match(source, /pendingConsultationQuestion: rectificationPendingQuestion/);
assert.match(chat, /pendingConsultationQuestion\?\.trim\(\)/);
// 旧 横幅承诺按钮会带回原问题 → 新 按钮已删,改成结束后新建对话再问 → 保留 pendingConsultationQuestion 传递链
assert.match(chat, /结束后新建对话,按采用的时间再问/);
});
test("ordinary consultation uses current birth data without a rectification notice", () => {
const source = homeSurface;
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, /<BirthTimeSoftNotice|setBirthTimeSoftNotice/);
assert.doesNotMatch(source, /setPendingBirthTimeChoice|<UnverifiedBirthTimeChoice/);
});
test("rectification mutations report pending state while session-level return controls stay absent", () => {
const source = homeSurface;
// 原值:onPendingChange={setRectificationMutationPending} 写在 page.tsx JSX
// 新值:onPendingChange: setRectificationMutationPending 写在 hook 的 panel
// 原因:mutationPending 仍由外壳对象持有,但 setter 经 panel 交给子树
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 = homeSurface;
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 = homeSurface;
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, /entrypoint: consultationEntrypointSchema\.exclude\(\["birth_time_rectification"\]\)\.optional\(\)/);
assert.doesNotMatch(source, /entrypoint: z\.literal\("daily_starlanguage"\)\.optional\(\)/);
assert.match(source, /question:\s*resolvedQuestion\.modelQuestion/);
// 原值: /先用 3–6 句口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
// 新值: /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
// 原因: 用户回合文案与新开场形状对齐(反差 / 谁推谁修 / 扮演哪个象 / 最多三条行动)。
assert.match(source, /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
assert.doesNotMatch(source, /JSON\.stringify\(toolInput\)/);
assert.match(source, /shouldLoadGeneralDailyPanchanga\(\{/);
assert.doesNotMatch(source, /用户明确选择的无出生分钟一般咨询/);
assert.match(source, /不要把整轮对话改成「一般知识或生时校正」二选一/);
});
// 原值:两个 `.product-entrypoint-hitarea` 绝对定位盖满卡片
// 新值:两个 `.starter-entry` 原生 button
// 原因:卡片降级成 pill 后,按钮本身就是可点区域,不再需要盖一层 hitarea。
// 意图不变:两个入口都是原生 button,且内部不嵌套第二个可点元素。
test("homepage entry points are exactly two native buttons with nothing nested inside", () => {
const source = homeSurface;
const entries = source.match(/className="starter-entry"/g) ?? [];
assert.equal(entries.length, 2);
assert.doesNotMatch(source, /product-entrypoint-hitarea/);
const row = source.slice(
source.indexOf('className="starter-entry-row"'),
source.indexOf("</div>", source.indexOf('className="starter-entry-row"')),
);
assert.doesNotMatch(row, /<a |onClick=\{[^}]*\}[\s\S]{0,40}<button/);
});
// 原值:锁 `.product-entrypoint-card:has(...:hover)` 不得有 background
// 新值:同一意图移到 `.starter-entry`
// 原因:卡片下线,但「hover 只动描边、按下才改底色」这条规则要跟着形态走——
// 一个会改底色的 hover 在触屏上会粘住,读起来像「已选中」。
test("entry pills use transient pressed feedback instead of sticky hover shading", () => {
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const hoverRules = [...styles.matchAll(/\.starter-entry:not\(:disabled\):hover\s*\{([^}]*)\}/g)];
assert.ok(hoverRules.length > 0, "the entry pill needs a hover rule");
for (const rule of hoverRules) assert.doesNotMatch(rule[1], /background\s*:/);
assert.match(styles, /\.starter-entry:not\(:disabled\):active\s*\{[^}]*background\s*:/);
assert.doesNotMatch(styles, /\.starter-theme-card|\.product-entrypoint-card/);
});
test("the starter heading is drawn per visit and the entry pills carry no fine print", () => {
const source = homeSurface;
// Given: the heading reads from a variant seeded once per mount and redrawn when the home is left.
// 原值 `{starterGreeting.question}` / 新值 `{starterGreeting.text}` / 原因:开场语改成单行问候,
// 「招呼语 + 问句」两半的形状取消,heading 直接就是整行;「每次进首页重抽」这条意图没变。
assert.match(source, /<h1 id="starter-heading">\{starterGreeting\.text\}<\/h1>/);
assert.match(source, /useState\(\(\) => Math\.random\(\)\)/);
assert.match(source, /starterGreetingUnseen\.current = true;\s*\n\s*setStarterGreetingSelection\(Math\.random\(\)\)/);
// 原值:校正卡片的 footer 里不得有 <small> 细则,且 action 靠 margin-left: auto 右对齐
// 新值:pill 里只有图标和一个词,细则根本没有容身处;一行合并提示在 pill 之下
// 原因:卡片下线。意图(入口上不堆小字)以更强的形式保住了。
const entryRow = source.slice(
source.indexOf('className="starter-entry-row"'),
source.indexOf('className="starter-entry-hint"'),
);
assert.doesNotMatch(entryRow, /<small>/);
assert.doesNotMatch(entryRow, /进度会自动保存/);
});
// 原值:锁手机断点下两张卡的 min-height / grid-template-rows / footer 折行 / action 左对齐
// 新值:pill 行自己会换行并保持居中
// 原因:被锁的那套卡片布局已不存在。原意图是「手机上别用固定高度、别把动作挤到右边」,
// pill 形态下由 flex-wrap 直接满足,不需要断点特判。
test("entry pills wrap instead of needing a phone-specific card layout", () => {
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const row = styles.match(/\.starter-entry-row \{([^}]*)\}/);
assert.ok(row);
assert.match(row[1], /flex-wrap:\s*wrap;/);
assert.match(row[1], /justify-content:\s*center;/);
assert.doesNotMatch(styles, /\.daily-starlanguage-card|\.birth-rectification-card/);
});
test("the starter greeting keeps the display stack and display weight", () => {
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const heading = styles.match(/\.starter-greeting-block h1 \{([^}]*)\}/)?.[1] ?? "";
// 原值:`.starter-hero h1` 与 `.product-entrypoint-copy h2`,字重 400
// 新值:`.starter-greeting-block h1`,字重 500
// 原因:hero 卡与入口卡下线;字重 400 是衬线时代的值,--font-display 改无衬线后发虚(BUG-737)。
assert.match(heading, /font-family:\s*var\(--font-display\)\s*;/);
assert.match(heading, /font-weight:\s*500\s*;/);
assert.doesNotMatch(heading, /font-family:\s*var\(--font-body\)/);
// 问候不该再是 52px 的落地页大标题,它现在只是输入框上面的一行。
// 原值 `clamp(26px,` / 新值 `clamp(24px,` / 原因:开场语从「招呼语 + 问句」两行收成单行问候,
// 不再需要 34px 上限那一档;上限同步降到 30px,断言仍然钉住「不是落地页大标题」这条意图。
assert.match(heading, /font-size:\s*clamp\(24px, 2\.8vw, 30px\);/);
assert.match(heading, /line-height:\s*1\.25;/);
});
test("the starter home hides technical chart parameters", () => {
const source = homeSurface;
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
// 原值:先 indexOf('<div className="starter-list starter-workbench"') 切片,再用
// /\/\* Starter workbench \*\/[\s\S]*?\.starter-list \{.../ 匹配 CSS。
// 新值:直接在整份 starter 源码上断言。
// 原因:那个切片起点消失后 indexOf 返回 -1,slice(-1) 得到最后一个字符,
// 随后的 [\s\S]*? 正则在 250KB CSS 上灾难性回溯——本条单测跑了 532 秒。
// 断言对象没有放宽,反而从「片段」扩大到了「整份文件」。
// 技术参数这一条只对 starter 组件本身断言:`homeSurface` 是十几个文件的拼接,
// 里面本来就含 `D1` 一类的标识符,拿整份去断会误伤(我第一版就踩了)。
const starter = readFileSync(new URL("../src/components/starter-home.tsx", import.meta.url), "utf8");
assert.doesNotMatch(source, /starter-theme-accordion|starterSuggestions/);
assert.doesNotMatch(starter, /evidencePreview|birthTimeDisplay|Vimshottari|D1|D9/);
assert.match(source, /composer-wrap-starter/);
assert.match(styles, /\.starter-entries \{/);
assert.doesNotMatch(styles, /Starter workbench/);
});
test("rectification 402 navigates to the membership page with the rectification source", () => {
// Given: the Agentic rectification chat owns the /api/rectification/agent call.
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
const membershipLib = readFileSync(new URL("../src/lib/membership.ts", import.meta.url), "utf8");
// 原值: 402 后 window.location.assign(membershipHref("rectification"))
// 新值: 留下说明并 onOpenBilling({ source: "rectification" }),不离开当前页
// 原因: 硬跳转清单只能缩小;对话状态不丢
assert.match(chat, /onOpenBilling\?\.\(\{ source: "rectification" \}\)/);
assert.match(chat, /if \(response\.status === 402\) \{[\s\S]*onOpenBilling\?\.\(\{ source: "rectification" \}\)/);
assert.doesNotMatch(chat, /window\.location\.assign\(membershipHref\(/);
assert.doesNotMatch(chat, /咨询点数不足/);
assert.doesNotMatch(chat, /setError\(`咨询点数不足/);
assert.match(membershipLib, /rectification: "生时校正需要点数或会员权益"/);
});