Files
Jyotisha/frontend/tests/consultation-entrypoint.test.ts
T
Jesse_ChenandClaude Opus 5 62903b7a12
Independent Staging Quality Gate / validate (push) Failing after 9m13s
Independent Staging Quality Gate / publish (push) Skipped
feat(ui): 空状态改成「问候 + 输入框 + 两枚入口 pill」
此前空态是 hero 卡(h1 clamp 到 52px)加两张 min-height 228px 的产品卡,
输入框被压在约 800px 内容之下,主动作不是视觉焦点。

改法:问候拆成 StarterGreeting 留在对话区并底对齐,两个产品入口降级成
StarterEntries 渲染在 composer 之后。composer 本身一行未动——它在空态与
会话态是同一个 DOM 位置,首条消息落地时不重挂载,草稿/焦点/排队卡都不受影响。

顺带修掉:空态 composer 1040px、会话态 760px,发首条消息时输入框会当场
变窄一截。两态统一到 --session-column-width,并新增断言锁住。

校正入口保留三个服务端驱动的文案分支(我第一版收成了两个,是
rectification-agentic-entry 打红才发现)。

DESIGN.md:Product entrypoint card 整节重写为 Starter home;删掉
Suggestion card 死文档(它描述的主题卡样式正是本轮删除的 CSS)。

测试 3350 不变,fail 仍 31 且与基线双向零差异;/ 仍 Static;
CSS gzip 41,096→41,044(−0.13%)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 05:31:15 +00:00

467 lines
25 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/);
assert.match(source, /先用 3–6 句口语直接回答下面的问题[\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.
assert.match(source, /<h1 id="starter-heading">\{starterGreeting\.question\}<\/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 的落地页大标题,它现在只是输入框上面的一行。
assert.match(heading, /font-size:\s*clamp\(26px,/);
});
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: "生时校正需要点数或会员权益"/);
});