From 0378d9e030cc7fb4dedc4dbddba4da1f7d8e409e Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 18 Sep 2026 10:39:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E6=A0=A1=E6=AD=A3=E5=AE=88=E5=8D=AB?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E4=BA=94=E6=9D=A1=E5=89=8D=E7=AB=AF=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E5=90=88=E5=90=8C=E8=B7=9F=E7=9D=80=E6=90=AC=E5=AE=B6?= =?UTF-8?q?=EF=BC=88BUG-940=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BUG_HISTORY.md | 18 +++++++++++++++++- ...SS-birth-time-journey-page-path-20260918.md | 15 +++++++++++---- docs/tasks/README.md | 2 +- frontend/tests/chat-composer-queue.test.ts | 5 ++++- frontend/tests/chat-session-url.test.ts | 11 +++++++++-- frontend/tests/consultation-entrypoint.test.ts | 12 ++++++++++-- frontend/tests/consultation-recovery.test.ts | 6 +++++- 7 files changed, 57 insertions(+), 12 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 495c355d..8c9be899 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -12298,6 +12298,22 @@ - 修复:首页表面改为必读 `(app)/page.tsx`,hooks 仍可选。新增路径锁:`PAGE.parts` 必须以 `(app)/page.tsx` 结尾,旧路径不得存在。 - 验证:`pytest tests/test_birth_time_journey_contract.py tests/test_session_management_entrypoints.py tests/test_supabase_user_data_contract.py tests/test_daily_and_rectification_entrypoints.py` 25 passed。未改产品代码。 - 防复发:扫首页源码的 Python 合同必须必读 `(app)/page.tsx`,不得对首页文件 `exists()` 跳过。外壳再搬家时,`CORE_PYTEST_TARGETS` 里所有 `_home_surface` 都要一起改。 -- 相关记录:BUG-927、BUG-933、BUG-934 +- 相关记录:BUG-927、BUG-933、BUG-934、BUG-940 - 复发自:BUG-933 - 修复版本:`39a0d7a9` + +## BUG-940 | 校正守卫后 5 条前端源码合同仍按旧写法断言,门禁继续红 + +- 状态:resolved +- 首次发现:2026-09-18 +- 最近更新:2026-09-18 +- 影响面:`chat-composer-queue.test.ts`、`chat-session-url.test.ts`、`consultation-entrypoint.test.ts`、`consultation-recovery.test.ts`;Gitea `backend-quality-gate` `npm test` +- 用户现象:BUG-939 修好 Python 合同后,门禁 [run 2764](https://git.copse.top/root/Jyotisha/actions/runs/2764) 仍红。`npm test` 3484 条里 5 条源码正则失败。线上仍停在 `dc2f2a16`。 +- 触发条件:push 到 `staging` 跑全量 `npm test`。先前被 Python 快速门挡住,这 5 条从未在门禁里跑到。 +- 根因:BUG-924/935 把校正会话判断收到 `composerLocksAsRectification` / `activateFallbackSession` / `dropRectificationStoredPending`。被保护的性质还在,合同仍按搬家前的字面量断言。BUG-933 只改了另外四条,漏了这五条。 +- 修复:按「原值 / 新值 / 原因」改写这五条,不断性质、不删测试。 +- 验证:上述四文件 `tsx --test` **58 passed / 0 failed**。未改产品代码。 +- 防复发:改 `page.tsx` 或校正守卫字面量的轮次必须跑全量 `npm test`,不得只跑定向。源码合同跟着实现搬家,见 BUG-927。 +- 相关记录:BUG-924、BUG-927、BUG-933、BUG-935、BUG-939 +- 复发自:BUG-933 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-birth-time-journey-page-path-20260918.md b/docs/tasks/PROGRESS-birth-time-journey-page-path-20260918.md index 728d24c8..aa7577d6 100644 --- a/docs/tasks/PROGRESS-birth-time-journey-page-path-20260918.md +++ b/docs/tasks/PROGRESS-birth-time-journey-page-path-20260918.md @@ -9,9 +9,10 @@ | 任务 | 状态 | 说明 | | --- | --- | --- | -| 路径搬家 BUG-939 | 完成 | `_home_surface` 必读 `(app)/page.tsx`;hooks 仍可选 | +| 路径搬家 BUG-939 | 完成 | `_home_surface` 必读 `(app)/page.tsx`;hooks 仍可选。合入 `39a0d7a9`,门禁 run 2764 已过 Python | | 路径锁 | 完成 | `PAGE.parts[-2:] == ("(app)", "page.tsx")`;旧路径不得存在 | -| 记录 | 完成 | BUG-939(复发自 BUG-933);BUG-933 相关记录补本条 | +| 五条前端合同 BUG-940 | 完成 | run 2764 暴露:校正守卫后源码正则未搬家 | +| 记录 | 完成 | BUG-939 / BUG-940(均复发自 BUG-933) | 未改 `page.tsx`、产品行为、Skill、workflow。 @@ -21,14 +22,20 @@ | --- | --- | --- | --- | | `_home_surface` 首页文件 | `frontend/src/app/page.tsx`,缺失则 `exists()` 跳过 | 必读 `frontend/src/app/(app)/page.tsx` | `e4e73f56` 已搬家;静默跳过把失败伪装成「找不到 ` { assert.match(startNewChat, /writeSessionUrl\(nextSession\.id, "push"\)/); assert.match(startNewChat, /window\.history\.replaceState\(null, "", previousHref\)/); + // 原值:delete/archive 函数体内直接 `writeSessionUrl(fallbackId || null, "replace")` + // 新值:删/归档当前会话走 `activateFallbackSession`,由它 `writeSessionUrl(null|fallbackId, "replace")` + // 原因:BUG-924 回退优先非校正会话,落到校正会话时要走 selectSession,不能在 delete 里写死 URL。 const deleteSession = sourceBetween(page, "async function deleteSession(", "function togglePinnedSession"); - assert.match(deleteSession, /writeSessionUrl\(fallbackId \|\| null, "replace"\)/); + assert.match(deleteSession, /if \(activeSessionId === session\.id\) \{\s*activateFallbackSession\(nextSessions\);/); const archiveSession = sourceBetween(page, "function toggleArchivedSession(", "async function shareSession"); - assert.match(archiveSession, /writeSessionUrl\(fallbackId \|\| null, "replace"\)/); + assert.match(archiveSession, /activateFallbackSession\(visibleSessions\.filter\(\(item\) => item\.id !== sessionId\)\)/); + + const fallback = sourceBetween(page, "function activateFallbackSession(", "async function loadMoreSessions"); + assert.match(fallback, /writeSessionUrl\(null, "replace"\)/); + assert.match(fallback, /writeSessionUrl\(fallbackId, "replace"\)/); const recovery = sourceBetween( page, diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts index f8a42177..858ea9c2 100644 --- a/frontend/tests/consultation-entrypoint.test.ts +++ b/frontend/tests/consultation-entrypoint.test.ts @@ -205,7 +205,10 @@ test("homepage opens through the server Case API and merges the returned session 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/); + // 原值:`const rectificationSurfaceOpen = activeRectificationSession && activeSession.id === rectificationSessionId` + // 新值:包一层 `Boolean(...)`,并先判 `activeSession`,避免无活跃会话时读 `.id` + // 原因:BUG-935 列表只剩校正时 activeSession 可为 undefined。 + assert.match(source, /const rectificationSurfaceOpen = Boolean\(activeRectificationSession && activeSession && activeSession\.id === rectificationSessionId\)/); // 原值:同一守卫内 JSX 写 pendingConsultationQuestion={rectificationPendingQuestion} // 新值:同一守卫内 JSX 写 panel={rectificationPanel};pending 字段在 hook panel // 原因:校正面子树状态收到 panel @@ -226,8 +229,13 @@ test("the page never creates the session shell locally; the server owns session test("rectification cards render only inside the active rectification session", () => { const source = homeSurface; + const guard = readFileSync(new URL("../src/lib/rectification-session-composer-guard.ts", import.meta.url), "utf8"); - assert.match(source, /activeSession\?\.sessionType === "birth_time_rectification"/); + // 原值:page 里 `activeSession?.sessionType === "birth_time_rectification"` + // 新值:Home 读 `composerLocksAsRectification(activeSession, sessions)`,类型判断在守卫 `session?.sessionType === "birth_time_rectification"` + // 原因:BUG-924/935 把「这是不是校正会话」收到守卫,卡片仍只在 `rectificationSurfaceOpen` 里渲染。 + assert.match(source, /composerLocksAsRectification\(activeSession, sessions\)/); + assert.match(guard, /session\?\.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>/); diff --git a/frontend/tests/consultation-recovery.test.ts b/frontend/tests/consultation-recovery.test.ts index 5dbc8dfa..4547f526 100644 --- a/frontend/tests/consultation-recovery.test.ts +++ b/frontend/tests/consultation-recovery.test.ts @@ -105,7 +105,11 @@ test("tab-local pending ids drive strict bootstrap recovery before the global fa ); assert.match(bootstrap, /readStoredPendingConsultation\([\s\S]*pendingConsultationStorageKey[\s\S]*nextSessions\.map\(\(session\) => session\.id\)/); - assert.match(bootstrap, /if \(!storedPending && sessionStorage\.getItem\(pendingConsultationStorageKey\)\) \{\s*sessionStorage\.removeItem\(pendingConsultationStorageKey\)/); + // 原值:`if (!storedPending && sessionStorage.getItem(...)) { sessionStorage.removeItem(...) }` + // 新值:`if (!storedPending) sessionStorage.removeItem(...)`,读存储前先 `dropRectificationStoredPending` + // 原因:校正会话上的 pending 不得进普通咨询恢复;无 pending 时直接清 key,不必再 getItem。 + assert.match(bootstrap, /dropRectificationStoredPending\([\s\S]*readStoredPendingConsultation/); + assert.match(bootstrap, /if \(!storedPending\) sessionStorage\.removeItem\(pendingConsultationStorageKey\)/); assert.ok(bootstrap.indexOf("if (storedPending)") < bootstrap.indexOf("fetchActiveConsultationStatus(controller.signal)")); assert.match(bootstrap, /if \(storedPending\) \{[\s\S]*fetchConsultationStatus\([\s\S]*storedPending\.sessionId,[\s\S]*storedPending\.requestId,[\s\S]*\} else \{[\s\S]*fetchActiveConsultationStatus/); assert.match(bootstrap, /status\.status === "reserved"[\s\S]*reservedConsultation = status;[\s\S]*else \{[\s\S]*sessionStorage\.removeItem\(pendingConsultationStorageKey\)/);