fix(web): 会话列表合同跟着外壳搬家,无活跃会话不再静默吞发送
四条源码合同改到 (app)/layout 与注册载荷两端对断。列表只剩校正会话时普通输入框锁定,发送会打开校正或给出提示。
This commit is contained in:
@@ -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=` 深链和刷新恢复,本轮只做服务端过滤 + 复用已有空咨询。
|
||||
|
||||
+50
-1
@@ -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` 把 `<main className="chat-app">` 和侧栏 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
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
@@ -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。
|
||||
@@ -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 不再写留白);真机六条欠 |
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<JumpToLatestButton onClick={conversationAnchor.anchorToLatest} />
|
||||
)}
|
||||
{rectificationComposerReopen && activeSession ? <RectificationComposerReopen message={rectificationErrorMessage} onReopen={() => void reopenRectificationSession(activeSession.id)} /> : null}
|
||||
{rectificationComposerReopen ? <RectificationComposerReopen message={rectificationErrorMessage} onReopen={() => void reopenRectificationSession(activeSession?.id || rectificationErrorSessionId || "")} /> : null}
|
||||
<ChatComposer
|
||||
inputRef={composerInput}
|
||||
inputLabel={!profileComplete && onboardingStep === "name" ? "输入你的称呼" : "输入你的问题"}
|
||||
|
||||
@@ -64,6 +64,7 @@ import { completedOnboardingTranscript, isProfileComplete, selectedBirthPlace }
|
||||
import { pendingConsultationStorageKey, timestamp } from "@/lib/home-types";
|
||||
import {
|
||||
consultSendBlockedByRectificationSession,
|
||||
missingActiveSessionSendAction,
|
||||
SESSION_NOT_CONSULTATION_CODE,
|
||||
} from "@/lib/rectification-session-composer-guard";
|
||||
import type {
|
||||
@@ -514,7 +515,19 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
? sessions.find((session) => 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 里 `<main className="chat-app">` 紧跟 replyAnnouncement 的 sr-only 播报
|
||||
// 新值:`<main className="chat-app">` 只在 (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(/<main className="chat-app">/g)?.length, 1);
|
||||
assert.doesNotMatch(pageFile, /<main className="chat-app">/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/<main className="chat-app">\n\s*<span className="sr-only" role="status" aria-live="polite" aria-atomic="true">\{replyAnnouncement\}<\/span>/,
|
||||
pageFile,
|
||||
/return \(\s*<>\s*<span className="sr-only" role="status" aria-live="polite" aria-atomic="true">\{replyAnnouncement\}<\/span>/,
|
||||
);
|
||||
|
||||
// Then: the region is fed a phase-derived string, not the streaming text.
|
||||
|
||||
@@ -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, "该对话不存在或已被删除");
|
||||
});
|
||||
|
||||
@@ -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\}/);
|
||||
});
|
||||
|
||||
@@ -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;/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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; \}/);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user