Files
Jyotisha/docs/tasks/TASK-archive-retire-fix-20260921.md
T
Jesse_ChenandClaude Opus 5 d3e0edfd1b docs(tasks): 归档下线验收结论与修复单(BUG-992)
5a4b8738 业务三层均正确;门禁 run 2836 红在两条 Python 源码合同
仍要求归档入口存在。同类第四次(933/934/939),防复发落到
frontend/AGENTS.md 的 grep 口径。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-21 19:09:44 +08:00

12 KiB
Raw Blame History

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. 开工前置命令

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,仍欠)。