diff --git a/docs/tasks/README.md b/docs/tasks/README.md index bd0c042b..b5ed9f32 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -154,7 +154,8 @@ | — | `PROGRESS-starter-greeting-20260917.md` | 首页开场语改成 claude.ai 式单行问候:`starter-greeting.ts` 的「称呼 + 追问句」五时段十五条收成三个池子(时段 / 星期 / 回访,按 `variantSelection` 确定性取一条),副标题行与 `.starter-salutation` 下线,h1 降到 `clamp(24px, 2.8vw, 30px)`;追问移到输入框占位符「想聊什么都可以」。产品直接拍板,非 Bug,不占 BUG 号 | 待验收 | `codex/starter-greeting-20260917` | | `TASK-session-list-rebuild-20260921.md` | `PROGRESS-session-list-rebuild-20260921.md` | **P0 会话列表重建(`e4e73f56` 的返工)**:服务端 `GET /api/sessions` 用 `messages <> '[]'` 排除空咨询,但校正会话的 `messages` 永远是 `[]`(建行写死空数组、创建合同只收空数组、PATCH 忽略、`touch_chat_session_from_rectification_case` 只 bump `updated_at`)→ **全部生时校正会话从侧栏消失**,而客户端 `isListedSidebarSession` 明确放行校正会话,两层规则相反且无跨层对断(BUG-987);同一提交把副标题换成 `created_at`,排序/分组/游标仍是 `updated_at`,列表可见时间不单调(9/18→9/17→9/7→9/16)(BUG-988);BUG-928 让步下来的「复用空会话」不 bump 时间,点新建变成跳回 9/17 的旧草稿(BUG-989)。**产品 09-21 拍板**:D1 副标题改最后活动时间(推翻 BUG-929 该条,其余保留)、D2 收掉 `BLOCKED.md:120` 的让步,第一问前不落库、删复用与 `draft`。T1 不得让步,T3 可拆轮。BUG 段 987 起 | **已验收**(经修复单) | `e71e4f92`(Claude 验收:T1/T2/T3 实现均正确;tsc 0 / lint 0 error 119 warning 与基线同 / `npm test` 本机 3640 条 34 红与 `f8d65e48` 逐条一致、净增 5 绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 643,115 较基线 −0.027% / page.tsx 1826→1791。**P1:门禁 run 2832 红、staging 未部署**——新加的真实 Postgres 测试在有 Docker 的门禁机上跑出 `unsupported not filter`,兼容层 `not()` 不支持 `is` 算子,归档分支炸。修复单 `TASK-session-list-rebuild-fix-20260921.md` 已验收通过,`df329fa9` 已部署) | | `TASK-session-list-rebuild-fix-20260921.md` | `PROGRESS-session-list-rebuild-fix-20260921.md` | **验收修复单(挡着三条修复上线)**:本地 PG 兼容层 `LocalPostgresQueryBuilder.not()` 只认 `eq` / `cs` 两个算子,`not("archived_at","is",null)` 直接 throw → `GET /api/sessions?archived=1` 落外层 catch 回 500,**侧栏「归档记录」自 `a1956deb`(09-06,BUG-553)起一直打不开**,两周无人发现是因为只有源码正则合同看着它;本轮新加的真实 Postgres 测试第一次真跑到这条路径,把它照出来,于是门禁 run 2832 红、`e71e4f92` 没部署。**决策**:在兼容层补 `is not null / is not true / is not false`,不得反过来改调用方绕开,不得删改测试断言让门禁变绿。与 BUG-926 的 `order()` 缺口同类。BUG 段 990 起 | **已验收通过** | `df329fa9`(Claude 验收:`not()` 增 `isNot` kind,统一走新的 `compileUnaryIsClause` 编译 `is [not] null/true/false`,非法取值仍 throw;`filterClause` 是唯一消费者、已覆盖,未落进兜底分支。**门禁 run 2833 全绿:3653 条 / 3653 过 / 0 红 / 0 skip**,`ok 1238` 即那条真实 Postgres 归档测试;migrate 2834、deploy 2835 成功,`/api/health` 的 `deployment.gitCommit` = `df329fa9`。本机复跑:tsc 0 / lint 0 error 119 warning 未增 / `npm test` 用例名逐条 diff 较 `e71e4f92` **恰好 +2 无删除**、失败清单完全一致 / `next build --webpack` 通过、`/` Static、首屏 gzip 643,116(+1 B)。**顺带查出 BUG-991**:归档视图的 UI 入口在 `92558ee6`(08-22)就被删了,`onToggleArchivedView` 全仓无调用点 → BUG-990 原描述「用户点归档记录后 500」不成立(接口层 500 属实,用户碰不到),已在 Bug 历史更正;归档后看不回来也恢复不了是真问题,待产品决策) | -| `TASK-archive-retire-20260921.md` | `PROGRESS-archive-retire-20260921.md` | **归档能力整体下线(BUG-991)**:行菜单能「归档」,归档后没有任何界面能列出或恢复它——切换按钮 `session-nav-toggle`(「归档 N」/「返回」)在 `92558ee6`(2026-08-22)被删,但 `onToggleArchivedView` / `archivedCount` / `showingArchived` / `toggleArchivedView()` / `GET /api/sessions?archived=1` / `applyArchiveFilter` 整条链路留着,全仓无渲染调用点;行菜单的「恢复」同样不可达(归档行永远不进 `visibleSessions`)。接口自 `a1956deb`(09-06)起就是 500(BUG-990),一个月没人发现正因为点不到。**产品 09-21 拍板整个下线**(口径:多余入口宁可删除也不修),不装回入口。D2 本轮不删 `archived_at` 列(§7.6 拆两轮),只清空存量;D3 不得回退 BUG-990 的兼容层修复;D4 存量归档会话一律放回列表。**迁移禁止 bump `updated_at`**,否则全部历史会话跳顶、毁掉刚修好的 BUG-988。让步顺序里 T3 必须先于 T1/T2。删除能力不动 | 待验收 | `codex/archive-retire-20260921` | +| `TASK-archive-retire-20260921.md` | `PROGRESS-archive-retire-20260921.md` | **归档能力整体下线(BUG-991)**:行菜单能「归档」,归档后没有任何界面能列出或恢复它——切换按钮 `session-nav-toggle`(「归档 N」/「返回」)在 `92558ee6`(2026-08-22)被删,但 `onToggleArchivedView` / `archivedCount` / `showingArchived` / `toggleArchivedView()` / `GET /api/sessions?archived=1` / `applyArchiveFilter` 整条链路留着,全仓无渲染调用点;行菜单的「恢复」同样不可达(归档行永远不进 `visibleSessions`)。接口自 `a1956deb`(09-06)起就是 500(BUG-990),一个月没人发现正因为点不到。**产品 09-21 拍板整个下线**(口径:多余入口宁可删除也不修),不装回入口。D2 本轮不删 `archived_at` 列(§7.6 拆两轮),只清空存量;D3 不得回退 BUG-990 的兼容层修复;D4 存量归档会话一律放回列表。**迁移禁止 bump `updated_at`**,否则全部历史会话跳顶、毁掉刚修好的 BUG-988。让步顺序里 T3 必须先于 T1/T2。删除能力不动 | **验收未通过(1 条 P1)** | `5a4b8738`(Claude 验收:**业务三层全部正确**——迁移只清 `archived_at`、无任何 `updated_at` 赋值、`schema_owner` 守卫、幂等;接口删 `?archived=1` 且 PATCH 收到 `archived_at` 回 2xx 不落库;界面行菜单只剩 收藏/重命名/转发/删除,`applyLegacySessionControls` 只删 archived 半边、留 pinned 半边;`local-postgres-not.test.ts` 与删除链路一字未动;`sidebar-contract.test.ts` 新增菜单项锁定断言。tsc 0 / lint 0 error 119 warning / `npm test` 3644 条用例名 diff **+3 无删除**、失败清单与 `df329fa9` 逐条一致 / `/` Static / 首屏 gzip 642,382(−0.11%)。**P1:门禁 run 2836 红在 Python**——`tests/test_session_management_entrypoints.py` 两条源码合同仍要求归档入口存在,本机复现恰好这两条。前序单的自查口径只写了 grep `frontend/src`,扫不到 `tests/` 下的 Python 合同。**同类第四次**(BUG-933/934/939)。修复单 `TASK-archive-retire-fix-20260921.md`) | +| `TASK-archive-retire-fix-20260921.md` | — | **验收修复单(挡着归档下线上线)**:`tests/test_session_management_entrypoints.py` 的 `test_chat_history_management_actions_are_exposed` 断言 `toggleArchivedSession` / `showArchivedSessions` / 「已归档,可在左侧归档中恢复。」/「归档」/「恢复」必须出现;`test_archiving_never_calls_the_delete_endpoint` 用 `source.index("function toggleArchivedSession")` 切片,函数没了直接 `ValueError`。**决策**:翻成反向断言而非删文件(反向断言只对 `sidebar-session-row.tsx` 与 `use-session-management.ts` 做,整份 `_home_surface()` 上做「恢复」会误伤);重写的测试必须保住与归档无关的 `method: mode === "create" ? "POST" : "PATCH"` 合同,并把「归档不走 DELETE」翻成「删除仍走 DELETE」。**F2 防复发**:`frontend/AGENTS.md` 加一条——删改前端符号/类名/可见文案前必须 `git grep -- tests/ frontend/` 而不是只扫 `frontend/`。BUG 段 992 起 | 待领取 | — | ### 个人报告 diff --git a/docs/tasks/TASK-archive-retire-fix-20260921.md b/docs/tasks/TASK-archive-retire-fix-20260921.md new file mode 100644 index 00000000..edbc239c --- /dev/null +++ b/docs/tasks/TASK-archive-retire-fix-20260921.md @@ -0,0 +1,181 @@ +# TASK 归档下线 · 验收修复单:两条 Python 源码合同仍要求归档入口存在,门禁红 + +- 日期:2026-09-21 +- 基线 commit:`5a4b8738`(`origin/staging` head;**未部署**,staging 仍停在 `df329fa9`) +- 分支:`codex/archive-retire-fix-20260921` +- BUG 编号起点:**BUG-992**(`docs/BUG_HISTORY.md` 当前最大号 `BUG-991`) +- 前序:`TASK-archive-retire-20260921.md` / `PROGRESS-archive-retire-20260921.md` +- 关联记录:**BUG-933、BUG-934、BUG-939**(同一类复发,第四次)、BUG-991 + +--- + +## 1. 事故实证 + +Gitea `backend-quality-gate` run **2836**(`5a4b8738`):`validate` 第 6 步失败,`publish` 跳过,**staging 未部署**。 + +红的**不是前端**,是 Python 那一段: + +``` +FAILED tests/test_session_management_entrypoints.py::test_chat_history_management_actions_are_exposed + - assert 'toggleArchivedSession' in '"use client";\n\nimport Link from "next/link"; …' +FAILED tests/test_session_management_entrypoints.py::test_archiving_never_calls_the_delete_endpoint + - ValueError: substring not found +== Quality gate failed == +``` + +我在本机 `python3 -m pytest tests/test_session_management_entrypoints.py -q` 复现,**恰好这两条,没有第三条**。 + +两条都是拿 Python 读前端源码做字符串断言的合同测试: + +- `test_chat_history_management_actions_are_exposed`:`_home_surface()`(`page.tsx` + 若干 `lib/` + 四个 hook)拼上 `globals.css` 与 `sidebar-session-row.tsx`,然后逐个断言一串 token 必须出现。其中五个是归档的:`toggleArchivedSession`、`showArchivedSessions`、`已归档,可在左侧归档中恢复。`、`归档`、`恢复`。 +- `test_archiving_never_calls_the_delete_endpoint`:`source.index("function toggleArchivedSession")` 切片,断言归档走 PATCH 而不是 DELETE。函数没了,`index()` 直接抛 `ValueError`。 + +前序单的验收标准里我写的是「`grep -rn "归档" frontend/src` 无命中」,执行方照做了并且做到了——**`tests/` 目录下的 Python 合同不在 `frontend/` 里,这条 grep 扫不到。** + +### 1.1 这是同一类问题的第四次 + +| 记录 | 现象 | +| --- | --- | +| BUG-933(09-17) | 四条针对 `page.tsx` 的源码合同没跟着外壳搬到 `(app)/layout.tsx`,门禁必红 | +| BUG-934(09-17) | `tests/test_daily_and_rectification_entrypoints.py` 仍找 `62903b7a` 已删除的卡片类名 | +| BUG-939(09-18) | `test_birth_time_journey_contract` 仍读已搬走的 `app/page.tsx`,staging 门禁全红 | +| **BUG-992(本条)** | `tests/test_session_management_entrypoints.py` 仍要求归档入口存在 | + +共同形状:**Python 测试对前端源码做字符串断言,而前端改动的自查只 grep `frontend/`**。改前端的人不知道 `tests/` 里有东西盯着自己。 + +--- + +## 2. 根因 + +两层: + +1. **直接原因**:前序单删了归档,但没同轮更新断言它存在的两条 Python 合同。 +2. **结构原因**:`tests/test_session_management_entrypoints.py` 把「这些入口必须存在」写成对前端源码文本的 `in` 断言。这种测试在**新增**时有价值(防止入口被误删),但在**有意删除**入口时必然反过来挡路,而且它待在 Python 目录里,前端改动者的自查半径覆盖不到。 + +--- + +## 3. 决策记录 + +- **D1:更新那两条测试,不是删掉整个文件。** 它挡住了这次交付,但它盯的其余入口(重命名、删除、确认框、收藏、转发、`share_payload_version`、删除路由的 `user_id` 与 `count !== 1`)都还有效。 +- **D2:归档的断言从「必须存在」翻成「必须不存在」。** 删了就删干净,留一条反向断言,避免以后谁又把归档加回来。 +- **D3:`test_archiving_never_calls_the_delete_endpoint` 整条重写而不是删除。** 它里面 `assert 'method: mode === "create" ? "POST" : "PATCH"' in write_contract` 与归档无关,是写入助手的独立合同,**必须搬进保留下来的测试里,不得随函数一起丢掉**。 +- **D4:不得为了让门禁绿而回退 `5a4b8738` 的任何业务改动。** 迁移、接口、界面三层都已验收正确(见 §6)。 + +--- + +## 4. 硬红线 + +1. 不得删除 `tests/test_session_management_entrypoints.py`,也不得把它整体改成宽松断言。 +2. D3 里那条 `writeChatSession` 的 POST/PATCH 合同必须活着。 +3. 不得改任何前端业务代码——本单只动 Python 测试与记录。 +4. 不得触碰会话删除相关的断言(`test_chat_session_delete_is_server_controlled_and_granted` 原样)。 + +--- + +## 5. 任务分解 + +### F1|更新两条 Python 合同(BUG-992) + +- `test_chat_history_management_actions_are_exposed`: + - 从必须出现的 token 列表里删掉 `toggleArchivedSession`、`showArchivedSessions`、`已归档,可在左侧归档中恢复。`、`归档`、`恢复`。 + 进度记录里按 AGENTS.md §7.3 写三栏:**原值** = 这五个 token 必须出现;**新值** = 必须不出现;**原因** = 归档能力于 2026-09-21 整体下线(BUG-991,产品拍板)。 + - 追加反向断言(D2):这五个 token 一个都不得出现在 `source` 里。注意 `恢复` 这类通用词可能在别处合法出现——**只对 `SESSION_ROW` 与 `use-session-management.ts` 两份源码做反向断言**,不要对整个 `_home_surface()` 做,否则会误伤。 +- `test_archiving_never_calls_the_delete_endpoint` → 重写并改名(例如 `test_session_archive_is_retired_and_write_helper_still_patches`): + - 断言 `_home_surface()` 里不存在 `function toggleArchivedSession` / `toggleArchivedView` / `archived_at:` 写入; + - 保留 `assert 'method: mode === "create" ? "POST" : "PATCH"' in write_contract`(D3); + - 保留「删除仍走 DELETE」的语义:断言 `deleteSession` 的切片里确有 `method: "DELETE"`(原测试是反向断言归档不含 DELETE,现在改成正向断言删除含 DELETE,覆盖面不降)。 +- **验收标准:** + - `python3 -m pytest tests/test_session_management_entrypoints.py -q` 全绿,测试函数数量不减。 + - 把任意一个归档 token 手工加回 `sidebar-session-row.tsx` 后该测试转红(执行方本地自测一次即可,不必提交这个临时改动)。 + - 门禁 run 转绿、`publish` 不再跳过。 + +### F2|防复发:改前端时把 `tests/` 也扫进去(BUG-992) + +这是第四次了,光写"记得改"没用。要落成可执行的东西: + +- 在 `frontend/AGENTS.md` 加一条:**删除或重命名任何前端符号、类名、可见文案之前,必须 `git grep -n "<符号>" -- tests/ frontend/`,而不是只 grep `frontend/`。** `tests/` 下有多条 Python 合同对前端源码做字符串断言(现存:`test_session_management_entrypoints.py`、`test_daily_and_rectification_entrypoints.py`、`test_birth_time_journey_contract.py`)。 +- `docs/BUG_HISTORY.md` 的 BUG-992 防复发写同一条,并关联 BUG-933 / 934 / 939。 +- **验收标准:** `frontend/AGENTS.md` 有这条;BUG-992 记录的「复发自」明确指向 BUG-939。 + +(**不做**:把这些 Python 合同搬进 `frontend/tests/`。那是另一轮的事,本单不扩范围。若执行方认为值得做,写进进度记录当建议,不要顺手动。) + +### F3|记录(BUG-992) + +- `docs/BUG_HISTORY.md` 新增 BUG-992;BUG-991 在门禁绿且部署后由验收方改 `resolved`,**本单不要替它改**。 +- `docs/tasks/README.md` 状态板加本单。 +- 不需要动 `CHANGELOG.md`(用户侧无变化,前序单已写过)。 + +--- + +## 6. 前序单(`5a4b8738`)的验收结论——业务三层全部正确,不要重做 + +我在 `.worktrees/verify-20260921` 用 `df329fa9` 作基线独立复跑: + +| 项 | 基线 `df329fa9` | `5a4b8738` | 结论 | +| --- | --- | --- | --- | +| `tsc --noEmit` | 0 错 | 0 错 | 通过 | +| `npm run lint` | 0 error / 119 warning | 0 error / 119 warning | 通过 | +| `npm test` | 3641 / 34 红 / 21 skip | 3644 / 34 红 / 22 skip | 失败清单 `diff` 逐条一致;用例名 `diff` **+3 无删除** | +| `next build --webpack` | 通过 | 通过 | `/` 仍 `○ Static` | +| 首屏 gzip | 643,116 | 642,382 | **−734 B / −0.11%** | +| Python 定向 | — | **2 红** | 见 §1,本单要修的就是这个 | + +新增的三条用例:`PATCH compatibility accepts and ignores a legacy archived_at write`、`session row menu is rename, pin, share and delete — not archive`、`retire archive migration clears archived_at without bumping updated_at`(本机无 Docker 时 skip)。 + +逐条对前序任务书: + +- **迁移(T3)正确**:只 `set archived_at = null where archived_at is not null`,**没有任何 `updated_at` 赋值**,`schema_owner` 守卫与既有迁移同形,幂等。红线 3 的核心(不得 bump `updated_at`,否则历史会话集体跳顶、毁掉 BUG-988)守住了。 +- **接口(T2)正确**:列表恒查 `is("archived_at", null)`;`?archived=1`、`isArchivedSessionQuery`、`applyArchiveFilter` 全删;`chatSessionMetadataPatchSchema` 去掉 `archived_at`,`extractChatSessionMetadataPatch` 读到但不放进 patch,`PATCH` 只带 `archived_at` 时走新的 `payloadHasIgnoredArchive` 分支回 `{ok:true}` 不落库——红线 6 的向后兼容做到了。 +- **界面(T1)正确**:行菜单只剩 收藏/取消收藏、重命名、转发、删除;`Archive` / `ArchiveRestore` import、`SidebarSession.archived`、`archivedCount`、`showingArchived`、`onToggleArchivedView`、`onToggleArchived`、`toggleArchivedSession`、`toggleArchivedView`、`showArchivedSessions` 全删;`visibleSessions` 收敛成单个 `isListedSidebarSession`(`useState` 少一个)。 +- **易漏点做到了**:`applyLegacySessionControls` 里 archived 的那一半删干净、pinned 那一半保留,`clearLegacySessionControlKeys` 仍清那个 localStorage key。 +- **红线 2 守住**:`local-postgres-not.test.ts` 一字未动。 +- **红线 1 守住**:`DELETE /api/sessions/[id]`、确认框、行菜单「删除」与其测试原样。 +- **防复发断言到位**:`sidebar-contract.test.ts` 新增的那条把菜单项锁成 `["重命名","转发","删除"]` + 动态收藏项,并反向断言无归档。 + +### 6.1 一条 P3(不挡本单,执行方可顺手,也可不做) + +新的迁移测试 `retire archive migration clears archived_at without bumping updated_at` **没有执行迁移文件本身**:它对文件正文做了两条正则(`/set archived_at = null/`、`doesNotMatch /updated_at\s*=/`),行为部分则手抄了一条同样的 `update` 语句去跑。参照 `rectification-session-title-repair-migration.test.ts` 的做法(SQL 从文件里切出来、与文件逐字比对),这里也可以把 `update` 语句从 `retireSql` 里切出来执行,避免文件与手抄语句漂移。现在的写法能挡住最要命的那条(bump `updated_at`),所以不算缺陷,只是可以更紧。 + +--- + +## 7. 交付前必须全跑 + +- `python3 -m pytest tests/test_session_management_entrypoints.py -q` → 全绿 +- `.venv/bin/python scripts/run_quality_gate.py --profile quick`(有 venv 时) +- 前端不改,但仍跑一遍 `tsc --noEmit` / `npm run lint` 确认没被带歪 +- `npm test` 全量 → 与 `5a4b8738` 的失败清单逐条一致(本机 3644 / 34 红 / 22 skip) + +--- + +## 8. 让步顺序 + +1. **无本地 Python venv** → 用系统 `python3` 跑那一个测试文件即可,`run_quality_gate.py` 跑不了就记进 `BLOCKED.md`,最终以门禁为准。 +2. F2 的 `frontend/AGENTS.md` 那条不得让步——它是这次唯一真正的防复发措施。 +3. F1 不得让步。 + +--- + +## 9. 开工前置命令 + +```bash +cd /workspace/Jyotisha +git status -sb | head -1 +git fetch origin --prune +git worktree add -b codex/archive-retire-fix-20260921 \ + .worktrees/archive-retire-fix-20260921 origin/staging +cd .worktrees/archive-retire-fix-20260921 +python3 -m pytest tests/test_session_management_entrypoints.py -q # 复现两条红 +git grep -n "toggleArchivedSession\|showArchivedSessions\|归档" -- tests/ frontend/ # 记录开工命中 +``` + +开工必读:`docs/BUG_HISTORY.md` 的 BUG-933、BUG-934、BUG-939(同类三次)、BUG-991;AGENTS.md §5、§7.3。 + +--- + +## 10. 部署后真人走查(产品负责人,本单与前序单合并验) + +1. 会话行「…」菜单里不再有「归档」。 +2. 以前归档过的对话重新出现在侧栏,且时间**没有**被改成今天。 +3. 删除会话仍然正常(含确认框)。 +4. 侧栏能看到 09-08 以来的生时校正会话,时间从上到下单调递减(BUG-987/988 的真机确认,仍欠)。 +5. 连点五次「新建对话」不堆出空行;发一句话后出现一行,时间是今天(BUG-989,仍欠)。