refactor(home): 校正面 15 个状态从 Home 下沉
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:
@@ -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 的 panel;page 仍 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 仍叫 rectificationErrorSessionId,setter 仍叫 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\)/);
|
||||
|
||||
Reference in New Issue
Block a user