refactor(home): 校正面 15 个状态从 Home 下沉
Independent Staging Quality Gate / validate (push) Failing after 6m43s
Independent Staging Quality Gate / publish (push) Skipped

Home() useState 66->52, useRef 41->39. Shell-facing fields merge into one object;
subtree state lives in useRectificationSurface. Chat props 20->7. Growth contract
rebaselined. Zero behavior/copy change.
This commit is contained in:
jesse-ux
2026-09-16 08:33:48 +08:00
parent 3b17c1b242
commit f8e607c29a
10 changed files with 448 additions and 122 deletions
+16 -4
View File
@@ -173,7 +173,10 @@ test("homepage birth-time card opens the V9 Agentic surface via the server case
assert.match(source, /openRectificationFromHomepage/);
assert.match(source, /<ConversationalBirthTimeRectification/);
assert.match(component, /<RectificationAgenticChat \{\.\.\.props\} \/>/);
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
// 原值: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/);
});
@@ -203,7 +206,10 @@ test("homepage opens through the server Case API and merges the returned session
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]*?<ConversationalBirthTimeRectification[\s\S]*?pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
// 原值:同一守卫内 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", () => {
@@ -278,7 +284,10 @@ 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");
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
// 原值: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, /结束后新建对话,按采用的时间再问/);
@@ -299,7 +308,10 @@ test("ordinary consultation uses current birth data without a rectification noti
test("rectification mutations report pending state while session-level return controls stay absent", () => {
const source = homeSurface;
assert.match(source, /onPendingChange=\{setRectificationMutationPending\}/);
// 原值: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, /返回并恢复原问题|返回首页/);
@@ -4,15 +4,16 @@ import test from "node:test";
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
// Measured 2026-09-16 on origin/staging @ 51a65d92.
// Line count: (pageSource.match(/\n/g) ?? []).length, same as `wc -l` = 1951.
// Measured 2026-09-16 after home-state-lowering on origin/staging @ 3b17c1b2.
// Line count: (pageSource.match(/\n/g) ?? []).length, same as `wc -l` = 1931.
// Hook counts use \buseState[<(] / \buseRef[<(] so `useState<Type>(` is not missed.
// Former line-count assertion lived in tests/chart-view-route.test.ts
// (`page.tsx does not grow to host the chart page`).
const PAGE_LINE_COUNT_BASELINE = 1951;
// Former caps were useState 66 / useRef 41 / lines 1951 on freeze-metric @ 51a65d92.
// This round moved 14 rectification useStates out of Home (15 → 1 shell object)
// and 2 rectification refs into useRectificationSurface.
const PAGE_LINE_COUNT_BASELINE = 1931;
const PAGE_LINE_COUNT_CAP = PAGE_LINE_COUNT_BASELINE + 150;
const HOME_USE_STATE_CAP = 66;
const HOME_USE_REF_CAP = 41;
const HOME_USE_STATE_CAP = 52;
const HOME_USE_REF_CAP = 39;
const USE_STATE_RE = /\buseState[<(]/g;
const USE_REF_RE = /\buseRef[<(]/g;
@@ -124,9 +124,15 @@ test("opening is server-owned: shouldStartOpening drives the first turn, never c
// 新:增加 skip_probe(核对卡「这题跳过」)
// 原因:决策 4,跳过只关本题
assert.match(route, /action: z\.enum\(\["opening", "message", "read_only", "answer_choice", "stop_and_review", "skip_probe"\]\)/);
assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
// 原值:shouldStartOpening={rectificationShouldStartOpening} 写在 page.tsx JSX
// 新值:shouldStartOpening: rectificationShouldStartOpening 写在 hook 的 panel
// 原因:开场开关只服务子树,随 panel 下沉
assert.match(page, /shouldStartOpening: rectificationShouldStartOpening/);
assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
// 原值:onOpeningConsumed={() => setRectificationShouldStartOpening(false)} 写在 page.tsx JSX
// 新值:onOpeningConsumed: () => setRectificationShouldStartOpening(false) 写在 hook 的 panel
// 原因:同上
assert.match(page, /onOpeningConsumed: \(\) => setRectificationShouldStartOpening\(false\)/);
});
test("incomplete profiles stay in the shared onboarding flow before any open request", () => {
@@ -221,9 +227,12 @@ test("persisted turns survive remounts; duplicate openings are suppressed by the
// Was: the "ready"/"loading" key suffix (see above). Persisted turns now survive
// because nothing remounts: the key is the session/Case binding only (BUG-505).
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
assert.match(page, /initialTurns=\{rectificationTurns\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
// 原值:initialTurns={rectificationTurns} / onMessagesChange={handleRectificationMessagesChange} / onOpeningConsumed={...} 写在 page.tsx JSX
// 新值:同名字段写在 hook 的 panel
// 原因:子树自有状态与回调收到 panel,Home 只传外壳还要读的绑定
assert.match(page, /initialTurns: rectificationTurns/);
assert.match(page, /onMessagesChange: handleRectificationMessagesChange/);
assert.match(page, /onOpeningConsumed: \(\) => setRectificationShouldStartOpening\(false\)/);
});
test("the agent route verifies the exact Case/Session binding before any turn", () => {
@@ -579,7 +588,10 @@ test("compact board overlays chat as a bottom sheet above the composer", () => {
assert.match(page, /className="chat-header-rectification"/);
assert.match(page, /data-rectification-header-slot=""/);
assert.match(page, /ref=\{setRectificationHeaderSlot\}/);
assert.match(page, /headerSlot=\{rectificationHeaderSlot\}/);
// 原值:headerSlot={rectificationHeaderSlot} 写在 page.tsx JSX
// 新值:headerSlot: rectificationHeaderSlot 写在 hook 的 panelpage 仍 ref={setRectificationHeaderSlot}
// 原因:header 节点在外壳,值只服务子树
assert.match(page, /headerSlot: rectificationHeaderSlot/);
assert.match(styles, /\.chat-header-rectification \.rectification-board-peek \{[\s\S]*width: auto/);
assert.doesNotMatch(chat, /rectification-workspace__board-trigger/);
assert.doesNotMatch(chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("<form className=\"composer\"")), /RectificationBoardPeek/);
@@ -46,7 +46,11 @@ test("a history-list open failure shows under the clicked row, not on the starte
// 原值:rectificationError 只由 starter-home 画;selectSession 不切会话,表现为点了没反应
// 新值:session 失败绑到被点的那一行;首页起始卡不重复同一条
// 决策 3:错误就地显示
assert.match(pageSource, /const \[rectificationErrorSessionId, setRectificationErrorSessionId\]/);
// 原值:const [rectificationErrorSessionId, setRectificationErrorSessionId] = useState(...)
// 新值:外壳 9 个校正字段合成一个 rectification 对象;errorSessionId 仍叫 rectificationErrorSessionIdsetter 仍叫 setRectificationErrorSessionId
// 原因:外壳也要读的校正状态合并成一个对象,散装 useState 下降
assert.match(pageSource, /errorSessionId: rectificationErrorSessionId,/);
assert.match(pageSource, /setRectificationErrorSessionId,/);
assert.match(
pageSource,
/rectificationError=\{rectificationErrorSessionId \? "" : rectificationError\}/,
@@ -35,7 +35,10 @@ test("the rectification surface is revealed once: Case hydration precedes the sw
// Panel: one key per binding, snapshot and turns as initial state, later turns as a prop update.
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
assert.doesNotMatch(page, /"ready" : "loading"/);
assert.match(page, /initialSnapshot=\{rectificationSnapshot\}/);
// 原值:initialSnapshot={rectificationSnapshot} 写在 page.tsx JSX
// 新值:initialSnapshot: rectificationSnapshot 写在 hook 的 panel
// 原因:snapshot 只服务子树,随 panel 下沉
assert.match(page, /initialSnapshot: rectificationSnapshot/);
assert.match(page, /declaredTime=\{rectificationDeclaredTime\}/);
assert.match(wrapper, /initialSnapshot: RectificationCaseSnapshotPayload \| null;/);
assert.match(chat, /useState\(initialSnapshot !== null\)/);