From db5b0a214bbb687f1a32e1fb65f7a70de5ded014 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 17 Sep 2026 23:06:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E4=BC=9A=E8=AF=9D=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=90=88=E5=90=8C=E8=B7=9F=E7=9D=80=E5=A4=96=E5=A3=B3?= =?UTF-8?q?=E6=90=AC=E5=AE=B6=EF=BC=8C=E6=97=A0=E6=B4=BB=E8=B7=83=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E4=B8=8D=E5=86=8D=E9=9D=99=E9=BB=98=E5=90=9E=E5=8F=91?= =?UTF-8?q?=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 四条源码合同改到 (app)/layout 与注册载荷两端对断。列表只剩校正会话时普通输入框锁定,发送会打开校正或给出提示。 --- BLOCKED.md | 6 +++ docs/BUG_HISTORY.md | 51 ++++++++++++++++++- ...session-list-single-source-fix-20260917.md | 30 +++++++++++ docs/tasks/README.md | 2 +- frontend/src/app/(app)/page.tsx | 6 +-- frontend/src/hooks/use-consultation-run.ts | 15 +++++- .../rectification-session-composer-guard.ts | 19 +++++++ .../chat-navigation-a11y-contract.test.ts | 12 +++-- frontend/tests/chat-session-url.test.ts | 11 +++- ...ectification-history-open-20260909.test.ts | 16 +++--- ...ctification-session-composer-guard.test.ts | 33 ++++++++++++ .../rectification-surface-contract.test.ts | 8 ++- ...est_daily_and_rectification_entrypoints.py | 12 ++++- 13 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 docs/tasks/PROGRESS-session-list-single-source-fix-20260917.md diff --git a/BLOCKED.md b/BLOCKED.md index ef342cbe..8b1bd177 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,11 @@ # BLOCKED +## 会话列表:元数据操作未上移到 provider(2026-09-17,BUG-927 让步) + +- **让步:** 任务书 T2 允许第一步只做「路由组 + layout 常驻 + provider 拥有列表 + 首页注册全部控制」。改名 / 置顶 / 归档 / 删除 / 翻页仍由首页注册,未挂载首页时侧栏只读。 +- **现状:** 四个页面共用一份列表、切页不重拉已落地。星盘 / 星历 / 报告页不能改名或删除。 +- **解除需要:** 把只依赖 `sessions` + fetch 的元数据操作搬进 `SessionListProvider`,四页都能用。另立单。 + ## 会话列表:空咨询延迟落库未做(2026-09-17,BUG-928) - **让步:** 任务书 T3 要把 `startNewChat` / 启动落点改成本地创建、第一问前才 `POST /api/sessions`。牵动 `?c=` 深链和刷新恢复,本轮只做服务端过滤 + 复用已有空咨询。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index db9ac96e..6cdbc646 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -12096,7 +12096,7 @@ - 根因:`/` 不在 `(secondary)` 路由组,离开即卸载 Home;次级页另用 `useSidebarData` 再拉一遍。 - 修复:四个页面进 `(app)` 路由组。一份 `SessionListProvider` 拉 `/api/sessions` 与 `/api/account`。首页不再自己 `fetchSessions`/`fetchAccount`。侧栏不随切页卸载。按任务书让步,改名/置顶/删除仍由首页注册,未注册时侧栏只读。 - 验证:`session-list-provider.test.ts`、`sidebar-contract.test.ts`、`chart-page-view.test.tsx`。 -- 防复发:`app/` 里 `SidebarProvider` 只许出现在 `(app)/layout.tsx`。 +- 防复发:`app/` 里 `SidebarProvider` 只许出现在 `(app)/layout.tsx`。外壳搬家必须同步搬走针对 `page.tsx` 的源码合同,且交付前必须跑全量 `npm test` 与 `tsc --noEmit`,不得只跑定向。 - 相关记录:BUG-745、BUG-926 - 复发自:无 - 修复版本:待发布 @@ -12180,3 +12180,52 @@ - 相关记录:BUG-930 - 复发自:无 - 修复版本:待发布 + +## BUG-933 | 四条源码合同没跟着外壳搬家,门禁卡住 + +- 状态:resolved +- 首次发现:2026-09-17 +- 最近更新:2026-09-17 +- 影响面:`chat-navigation-a11y-contract.test.ts`、`rectification-history-open-20260909.test.ts`、`rectification-surface-contract.test.ts`、`chat-session-url.test.ts` +- 用户现象:会话列表单实现已落地,但 staging 门禁 `npm test` 必红,今天的代码提交上不了线。 +- 触发条件:`e4e73f56` 把 `
` 和侧栏 props 搬到 `(app)/layout.tsx`,启动改成 `sessionListReady`。 +- 根因:四条合同仍按搬家前的 `page.tsx` 形状断言。被保护的性质都还在,是合同陈旧。执行方只跑了定向测试。 +- 修复:四条按「原值 / 新值 / 原因」改写,分别断 layout 与 homepage 注册两端,不得删测试。 +- 验证:上述四文件;全量 `npm test` 相对 `cc1a8980` 少这 4 条红。 +- 防复发:见 BUG-927。 +- 相关记录:BUG-927、BUG-745 +- 复发自:无 +- 修复版本:待发布 + +## BUG-934 | 今日星语 / 生时校正入口合同仍找已删除的卡片类名 + +- 状态:resolved +- 首次发现:2026-09-17 +- 最近更新:2026-09-17 +- 影响面:`tests/test_daily_and_rectification_entrypoints.py` +- 用户现象:Python 定向跑这两条必红。不在 `CORE_PYTEST_TARGETS`,不挡门禁。 +- 触发条件:断言 `daily-starlanguage-card` / `birth-rectification-card`。 +- 根因:`62903b7a` 把空状态改成问候 + 输入框 + 两枚 pill,类名已全仓删除。既有欠账,不是会话列表单引入。 +- 修复:改为断言 `starter-entry` pill 与 `startDailyStarlanguageConsultation` / `openRectificationFromHomepage`,并断言旧类名不存在。 +- 验证:`pytest tests/test_daily_and_rectification_entrypoints.py`。 +- 防复发:入口形态变了必须改这条合同,不得把已删除的卡片类名当存在。 +- 相关记录:无 +- 复发自:无 +- 修复版本:待发布 + +## BUG-935 | 列表只剩校正会话且无活跃会话时,普通输入框静默吞发送 + +- 状态:resolved +- 首次发现:2026-09-17 +- 最近更新:2026-09-17 +- 影响面:`page.tsx` `activeSession` 回退、`send()`、普通 `ChatComposer` +- 用户现象:账号里只有校正会话时,普通输入框能打字;发送没有任何提示,字消失。 +- 触发条件:`activeSession` 为 `undefined`(列表全是校正,找不到非校正回退),校正面正在打开或打开失败。 +- 根因:校正守卫把 `?? sessions[0]` 改成 `find(非校正)`。无活跃会话时 `activeRectificationSession` 为假,输入框可用;`send()` 因 `!currentSession` 直接 `return false`。 +- 修复:`composerLocksAsRectification`:无活跃会话但列表有校正会话时按校正占位锁定。`send()` 无活跃会话时打开校正或提示「请先选一条对话」。 +- 验证:`rectification-session-composer-guard.test.ts`。 +- 防复发:无活跃会话不得静默吞发送;列表全是校正时普通输入框不得可用。 +- 相关记录:BUG-924 +- 复发自:无 +- 修复版本:待发布 + diff --git a/docs/tasks/PROGRESS-session-list-single-source-fix-20260917.md b/docs/tasks/PROGRESS-session-list-single-source-fix-20260917.md new file mode 100644 index 00000000..f179daff --- /dev/null +++ b/docs/tasks/PROGRESS-session-list-single-source-fix-20260917.md @@ -0,0 +1,30 @@ +# PROGRESS · 会话列表验收修复单(2026-09-17) + +工作树:`.worktrees/session-list-single-source-fix-20260917` +分支:`codex/session-list-single-source-fix-20260917` +基线:`origin/staging` @ `38cb81ff` + +## 任务状态 + +| 任务 | 状态 | 说明 | +| --- | --- | --- | +| F1 四条合同搬家 BUG-933 | 完成 | 不断性质,改断言到 layout + 注册载荷 | +| F2 Python 入口合同 BUG-934 | 完成 | 改为 `starter-entry` pill,断言旧卡片类名不存在 | +| F3 无活跃会话吞发送 BUG-935 | 完成 | `composerLocksAsRectification` + `send()` 给反馈 | +| F4 记录 | 完成 | BUG-927 防复发已补;元数据操作让步进 `BLOCKED.md` | + +`page.tsx` 行数:开工 1824,结束 1824。 + +## 测试 + +| 项 | 结果 | +| --- | --- | +| `tsc --noEmit` | 0 错 | +| `npm run lint` | 0 error(118 warning,未动) | +| 四条目标合同 | `chat-navigation-a11y` / `rectification-history-open` / `rectification-surface-contract` / `chat-session-url`「home stays a client-read query」全绿 | +| F3 | `rectification-session-composer-guard` 新断言绿 | +| F2 | `pytest tests/test_daily_and_rectification_entrypoints.py` **4 passed** | +| 全量 `npm test` | 本机 Windows worktree **3221 / 3115 pass / 92 fail / 14 skip**。任务点名的 4 条不在失败清单。多出来的失败主要是:skill runtime `EPERM` 建 symlink、esbuild `spawn UNKNOWN`(junction 指到别的 worktree 的 esbuild)、无 Docker 的 database/deploy 套件。与验收机 `cc1a8980` 的 3457 / 39 红不可直接逐条对比。`creating and leaving a session keep the address bar in sync` 仍红:`deleteSession` 已走 `activateFallbackSession`(composer-guard 起),不是本单引入 | +| `next build --webpack` | webpack **Compiled successfully**;收集 `/api/birth-time-guide` 页数据时 Windows `EPERM` symlink 失败。`/` 是否 Static 未在本机核到。门禁 Linux 机应能过编译 | + +未改排序、provider、路由组。`page.tsx` 1824 → 1824。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 42590027..ad473dc8 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -132,7 +132,7 @@ | `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal` 的 `dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` | | `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待验收 | `codex/consult-followup-tool-contract-20260917` | | `TASK-session-list-single-source-20260917.md` | `PROGRESS-session-list-single-source-20260917.md` | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动(产品拍板:首页与三个次级页进同一路由组,侧栏外壳与列表 provider 常驻 layout,不做 store);空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 验收未通过 | `e4e73f56`(实现按任务书落地:多键排序 / `(app)` 路由组常驻外壳 / provider / 空会话不入列 + draft 复用 / 标题类别在前;两条让步任务书允许。**P1:4 条针对 `page.tsx` 的源码合同没跟着外壳搬家,`npm test` 在门禁里 `set -e` 下必红,staging 至今停在 `dc2f2a16`**。另:进度记录报「tsc 0 错」实为 2 错,只跑了定向测试。修复单 `TASK-session-list-single-source-fix-20260917.md`) | -| `TASK-session-list-single-source-fix-20260917.md` | — | 验收修复单:F1 四条源码合同搬到 `(app)/layout.tsx` 两端对断(BUG-933);F2 两条陈旧 Python 入口断言(BUG-934,既有欠账);F3 无活跃会话时输入框静默吞发送(BUG-935);交付前必须跑全量测试 | 待领取 | — | +| `TASK-session-list-single-source-fix-20260917.md` | `PROGRESS-session-list-single-source-fix-20260917.md` | 验收修复单:F1 四条源码合同搬到 `(app)/layout.tsx` 两端对断(BUG-933);F2 两条陈旧 Python 入口断言(BUG-934,既有欠账);F3 无活跃会话时输入框静默吞发送(BUG-935);交付前必须跑全量测试 | 待验收 | `codex/session-list-single-source-fix-20260917` | | `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` | | `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 | diff --git a/frontend/src/app/(app)/page.tsx b/frontend/src/app/(app)/page.tsx index d9ff7d34..44b994d9 100644 --- a/frontend/src/app/(app)/page.tsx +++ b/frontend/src/app/(app)/page.tsx @@ -20,7 +20,7 @@ import { resolveRectificationEntryAction, } from "@/lib/rectification-entry"; import { - dropRectificationStoredPending, + composerLocksAsRectification, dropRectificationStoredPending, ordinaryComposerPlaceholder, rectificationOrdinaryComposerReopen, } from "@/lib/rectification-session-composer-guard"; @@ -391,7 +391,7 @@ export default function Home() { const activeSession = sessions.find((session) => session.id === activeSessionId) ?? sessions.find((session) => session.sessionType !== "birth_time_rectification"); - const activeRectificationSession = activeSession?.sessionType === "birth_time_rectification"; + const activeRectificationSession = composerLocksAsRectification(activeSession, sessions); const rectificationSurfaceOpen = Boolean(activeRectificationSession && activeSession && activeSession.id === rectificationSessionId); const rectificationComposerLocked = Boolean(activeRectificationSession && !rectificationSurfaceOpen); const rectificationComposerReopen = rectificationOrdinaryComposerReopen({ activeRectificationSession, error: rectificationError, errorSessionId: rectificationErrorSessionId, sessionId: activeSession?.id }); @@ -1745,7 +1745,7 @@ export default function Home() { {jumpToLatestVisible && ( )} - {rectificationComposerReopen && activeSession ? void reopenRectificationSession(activeSession.id)} /> : null} + {rectificationComposerReopen ? void reopenRectificationSession(activeSession?.id || rectificationErrorSessionId || "")} /> : null} session.id === targetSessionId) : activeSession; const currentSession = options.sessionOverride ?? liveSession; - if (!question || !currentSession || !modelCatalog || !account) return false; + if (!question) return false; + if (!currentSession) { + const action = missingActiveSessionSendAction(sessions); + if ("openSessionId" in action) { + setDraft(originalQuestion); + await openRectificationSession(action.openSessionId); + setDraft(originalQuestion); + return false; + } + setComposerNotice(action.notice); + return false; + } + if (!modelCatalog || !account) return false; if (consultSendBlockedByRectificationSession(currentSession)) { if (resuming) { pendingConsultation.current = null; diff --git a/frontend/src/lib/rectification-session-composer-guard.ts b/frontend/src/lib/rectification-session-composer-guard.ts index da5a8196..72e5dbc0 100644 --- a/frontend/src/lib/rectification-session-composer-guard.ts +++ b/frontend/src/lib/rectification-session-composer-guard.ts @@ -23,6 +23,24 @@ export function consultSendBlockedByRectificationSession( return isRectificationChatSession(session); } +/** Empty active session with only rectification rows still occupies the ordinary composer. */ +export function composerLocksAsRectification( + activeSession: { sessionType?: string } | null | undefined, + sessions: readonly { sessionType: string }[], +): boolean { + if (isRectificationChatSession(activeSession)) return true; + if (activeSession) return false; + return sessions.some((session) => session.sessionType === "birth_time_rectification"); +} + +export function missingActiveSessionSendAction( + sessions: readonly { id: string; sessionType: string }[], +): { openSessionId: string } | { notice: string } { + const rectification = sessions.find((session) => session.sessionType === "birth_time_rectification"); + if (rectification) return { openSessionId: rectification.id }; + return { notice: "请先选一条对话,或点上方「新建对话」。" }; +} + export function dropRectificationStoredPending< S extends { sessionId: string }, T extends { id: string; sessionType: string }, @@ -63,5 +81,6 @@ export function rectificationOrdinaryComposerReopen(input: { if (!input.activeRectificationSession || !input.error || input.error === "profile_incomplete") { return false; } + if (!input.sessionId) return true; return !input.errorSessionId || input.errorSessionId === input.sessionId; } diff --git a/frontend/tests/chat-navigation-a11y-contract.test.ts b/frontend/tests/chat-navigation-a11y-contract.test.ts index 6e30ef35..cf839314 100644 --- a/frontend/tests/chat-navigation-a11y-contract.test.ts +++ b/frontend/tests/chat-navigation-a11y-contract.test.ts @@ -108,10 +108,16 @@ test("bootstrap failure retries with a hard reload because refresh cannot rerun }); test("the streaming reply announces state transitions, never streamed deltas", () => { - // Given: one page-level status region outside every aria-busy subtree. + // 原值:page.tsx 里 `
` 紧跟 replyAnnouncement 的 sr-only 播报 + // 新值:`
` 只在 (app)/layout.tsx 出现一次;播报仍在 page.tsx,是 SidebarInset 内容的第一个子节点 + // 原因:T2 外壳搬家,播报仍在对话页、仍在 aria-busy 列表之外 + const layout = readFileSync(new URL("../src/app/(app)/layout.tsx", import.meta.url), "utf8"); + const pageFile = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8"); + assert.equal(layout.match(/
/g)?.length, 1); + assert.doesNotMatch(pageFile, /
/); assert.match( - pageSource, - /
\n\s*\{replyAnnouncement\}<\/span>/, + pageFile, + /return \(\s*<>\s*\{replyAnnouncement\}<\/span>/, ); // Then: the region is fed a phase-derived string, not the streaming text. diff --git a/frontend/tests/chat-session-url.test.ts b/frontend/tests/chat-session-url.test.ts index de01fdd1..ff7d7810 100644 --- a/frontend/tests/chat-session-url.test.ts +++ b/frontend/tests/chat-session-url.test.ts @@ -213,12 +213,19 @@ test("home stays a client-read query on a static route", () => { assert.match(page, /window\.history\.replaceState/); assert.match(lib, /history\.pushState\(null, "", next\)/); assert.match(lib, /history\.replaceState\(null, "", next\)/); + // 原值:首页自己并发拉账户/目录/列表,`const [nextAccount, modelCatalogResult, sessionsPayload]` + // 新值:`await sessionListReady` 后读 `sessionListBoot()`,page.tsx 不再 `fetchSessions(` + // 原因:T2 列表由 layout provider 拉一次;本条仍守「`/` 是静态路由上的客户端读」 + const pageFile = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8"); const preview = sourceBetween( - page, + pageFile, "if (previewMode) {", - "const [nextAccount, modelCatalogResult, sessionsPayload]", + "await sessionListReady;", ); assert.doesNotMatch(preview, /writeSessionUrl|persistLoginSessionReturn|readLoginSessionReturn/); + assert.match(pageFile, /await sessionListReady;/); + assert.match(pageFile, /sessionListBoot\(\)/); + assert.doesNotMatch(pageFile, /fetchSessions\(/); assert.match(page, /SESSION_MISSING_NOTICE/); assert.equal(SESSION_MISSING_NOTICE, "该对话不存在或已被删除"); }); diff --git a/frontend/tests/rectification-history-open-20260909.test.ts b/frontend/tests/rectification-history-open-20260909.test.ts index 34ae08ef..703476ac 100644 --- a/frontend/tests/rectification-history-open-20260909.test.ts +++ b/frontend/tests/rectification-history-open-20260909.test.ts @@ -55,11 +55,15 @@ test("a history-list open failure shows under the clicked row, not on the starte pageSource, /rectificationError=\{rectificationErrorSessionId \? "" : rectificationError\}/, ); - assert.match(pageSource, /openErrorSessionId=\{rectificationErrorSessionId\}/); - assert.match( - pageSource, - /openErrorMessage=\{rectificationErrorSessionId \? rectificationErrorMessage : ""\}/, - ); + // 原值:page.tsx 把 openErrorSessionId / openErrorMessage 直接传给 AppSidebar + // 新值:layout 从 registration 传给 AppSidebar,首页 registerShellControls 载荷带这两个字段 + // 原因:T2 外壳搬家,两端都断才等于侧栏行仍拿得到打开失败 + const layout = read("../src/app/(app)/layout.tsx"); + const shell = read("../src/hooks/use-home-shell-registration.ts"); + assert.match(layout, /openErrorSessionId=\{registration\?\.openErrorSessionId \?\? null\}/); + assert.match(layout, /openErrorMessage=\{registration\?\.openErrorMessage \?\? ""\}/); + assert.match(shell, /openErrorSessionId: rectificationErrorSessionId,/); + assert.match(shell, /openErrorMessage: rectificationErrorSessionId \? rectificationErrorMessage : "",/); assert.match(starter, /rectificationError && !rectificationSurfaceOpen && \(/); assert.match(sidebar, /openErrorSessionId/); assert.match(sidebarRow, /error \? \(/); @@ -70,5 +74,5 @@ test("a history-list open failure shows under the clicked row, not on the starte const notice = cssDeclarations(".session-open-error", styles); assert.match(notice, /color:\s*var\(--color-danger\)/); assert.match(notice, /font-size:\s*var\(--type-overline\)/); - assert.match(page, /openErrorSessionId=\{rectificationErrorSessionId\}/); + assert.match(layout, /openErrorSessionId=\{registration\?\.openErrorSessionId \?\? null\}/); }); diff --git a/frontend/tests/rectification-session-composer-guard.test.ts b/frontend/tests/rectification-session-composer-guard.test.ts index 5812d4c2..3201e2be 100644 --- a/frontend/tests/rectification-session-composer-guard.test.ts +++ b/frontend/tests/rectification-session-composer-guard.test.ts @@ -4,9 +4,11 @@ import test from "node:test"; import { homeSurface as page } from "./home-surface.ts"; import { + composerLocksAsRectification, consultSendBlockedByRectificationSession, dropRectificationStoredPending, fallbackSessionId, + missingActiveSessionSendAction, RECTIFICATION_OPENING_COMPOSER_PLACEHOLDER, RECTIFICATION_REOPEN_LABEL, rectificationOrdinaryComposerReopen, @@ -104,3 +106,34 @@ test("delete and popstate fallbacks do not setActiveSessionId onto a rectificati assert.match(sessions, /const fallbackId = fallbackSessionId\(listed\);/); assert.match(page, /sessions.find\(\(session\) => session.sessionType !== "birth_time_rectification"\)/); }); + +test("an empty active session still locks the composer when the list is only rectification", () => { + assert.equal( + composerLocksAsRectification(undefined, [{ sessionType: "birth_time_rectification" }]), + true, + ); + assert.equal( + composerLocksAsRectification(undefined, [{ sessionType: "consultation" }]), + false, + ); + assert.equal( + composerLocksAsRectification({ sessionType: "consultation" }, [{ sessionType: "birth_time_rectification" }]), + false, + ); + assert.deepEqual( + missingActiveSessionSendAction([{ id: "r1", sessionType: "birth_time_rectification" }]), + { openSessionId: "r1" }, + ); + assert.deepEqual( + missingActiveSessionSendAction([]), + { notice: "请先选一条对话,或点上方「新建对话」。" }, + ); + assert.match(pageSource, /composerLocksAsRectification\(activeSession, sessions\)/); + assert.match(sendFn, /missingActiveSessionSendAction\(sessions\)/); + assert.match(sendFn, /openRectificationSession\(action\.openSessionId\)/); + assert.match(sendFn, /setComposerNotice\(action\.notice\)/); + assert.doesNotMatch( + sendFn, + /if \(!question \|\| !currentSession \|\| !modelCatalog \|\| !account\) return false;/, + ); +}); diff --git a/frontend/tests/rectification-surface-contract.test.ts b/frontend/tests/rectification-surface-contract.test.ts index c12d1676..2e503b49 100644 --- a/frontend/tests/rectification-surface-contract.test.ts +++ b/frontend/tests/rectification-surface-contract.test.ts @@ -82,7 +82,13 @@ test("entry feedback is static: card and sidebar row say 正在打开, and nothi assert.doesNotMatch(starter, /InlineSpinner/); assert.match(sidebarRow, /aria-busy=\{opening \? true : undefined\}/); assert.match(sidebarRow, /RECTIFICATION_SIDEBAR_OPENING_NOTE/); - assert.match(page, /openingSessionId=\{rectificationOpeningSessionId\}/); + // 原值:page.tsx 把 openingSessionId 直接传给 AppSidebar + // 新值:layout 从 registration 传入,首页 registerShellControls 载荷带 openingSessionId + // 原因:T2 外壳搬家,两端都断才等于侧栏行仍拿得到正在打开 + const layout = read("../src/app/(app)/layout.tsx"); + const shell = read("../src/hooks/use-home-shell-registration.ts"); + assert.match(layout, /openingSessionId=\{registration\?\.openingSessionId \?\? null\}/); + assert.match(shell, /openingSessionId: rectificationOpeningSessionId,/); // 原值 `.product-entrypoint-card[data-opening="true"]` / 新值 `.starter-entry[data-opening="true"]` // / 原因:入口从卡片降级成 pill。「打开 Case 时只换指针、不出 spinner」这条不变。 assert.match(styles, /\.starter-entry\[data-opening="true"\][^{]*\{ cursor: progress; \}/); diff --git a/tests/test_daily_and_rectification_entrypoints.py b/tests/test_daily_and_rectification_entrypoints.py index cbf4754a..8465804a 100644 --- a/tests/test_daily_and_rectification_entrypoints.py +++ b/tests/test_daily_and_rectification_entrypoints.py @@ -17,13 +17,17 @@ def _home_entrypoint_surface() -> str: def test_daily_starlanguage_entrypoint_is_productized() -> None: + # 原值:断言 daily-starlanguage-card(空状态大卡) + # 新值:StarterEntries 的 starter-entry pill + startDailyStarlanguageConsultation + # 原因:62903b7a 已把卡片改成问候 + 输入框 + 两枚 pill,全仓已无该类名 source = _home_entrypoint_surface() assert "今日星语" in source assert "fetchDailyStarlanguage" in source - assert "daily-starlanguage-card" in source + assert "starter-entry" in source assert "startDailyStarlanguageConsultation" in source assert "从今日问起" in source assert "今天的星语还没写出来。" in source + assert "daily-starlanguage-card" not in source assert "buildDailyStarlanguageCard" not in source assert "点这里" not in source assert "不会用通用文案顶替" not in source @@ -40,10 +44,14 @@ def test_daily_starlanguage_api_declares_honest_source_boundary() -> None: def test_birth_time_rectification_entrypoint_is_productized() -> None: + # 原值:断言 birth-rectification-card + # 新值:同一套 starter-entry pill + openRectificationFromHomepage + # 原因:同上,卡片入口已删除 source = _home_entrypoint_surface() assert "生时校正" in source - assert "birth-rectification-card" in source + assert "starter-entry" in source assert "openRectificationFromHomepage" in source + assert "birth-rectification-card" not in source def test_birth_rectification_api_proxies_active_questionnaire_with_boundary() -> None: