test: stop requiring retired archive entrypoints in Python contracts
Independent Staging Quality Gate / validate (push) Successful in 11m24s
Independent Staging Quality Gate / publish (push) Successful in 3m45s

Flip archive tokens to must-not-appear on the session row and session hook only. Keep writeChatSession POST/PATCH and assert delete still uses DELETE. Grep tests/ before deleting frontend symbols.
This commit is contained in:
jesse-ux
2026-09-21 19:20:45 +08:00
parent d3e0edfd1b
commit d2cec17995
6 changed files with 85 additions and 22 deletions
+16
View File
@@ -13135,3 +13135,19 @@
- 相关记录:BUG-990、BUG-553
- 复发自:无
- 修复版本:待本修复合入 staging 且门禁转绿
## BUG-992 | 归档下线后门禁 Python 合同仍要求归档入口存在
- 状态:resolved
- 首次发现:2026-09-21
- 最近更新:2026-09-21
- 影响面:`tests/test_session_management_entrypoints.py`
- 用户现象:无直接用户现象。Gitea 门禁 run 2836 红,`5a4b8738` 未部署。
- 触发条件:向 staging 推送删掉归档入口的前端改动。
- 根因:Python 合同对前端源码做字符串断言,要求 `toggleArchivedSession` 等必须出现。前序单 grep 只扫 `frontend/`,扫不到 `tests/`。同类第四次。
- 修复:那五个 token 从「必须出现」翻成「必须不出现」,且只对 `sidebar-session-row.tsx``use-session-management.ts` 做反向断言。归档测试重写,保住 `writeChatSession` 的 POST/PATCH 合同,并把「归档不走 DELETE」改成「删除仍走 DELETE」。`frontend/AGENTS.md` 要求删符号前 `git grep -- tests/ frontend/`
- 验证:`python -m pytest tests/test_session_management_entrypoints.py`;门禁以合入后的 run 为准。
- 防复发:删除或重命名任何前端符号、类名、可见文案之前,必须 `git grep -n "<符号>" -- tests/ frontend/`,不得只 grep `frontend/`
- 相关记录:BUG-933、BUG-934、BUG-939、BUG-991
- 复发自:BUG-939(Python 合同盯着已搬走的前端源码)
- 修复版本:待本修复合入 staging
@@ -0,0 +1,33 @@
# PROGRESS · 归档下线验收修复单(2026-09-21)
工作树:`.worktrees/archive-retire-fix-20260921`
分支:`codex/archive-retire-fix-20260921`
基线:`origin/staging` @ `d3e0edfd`(修复单;代码基线 `5a4b8738`
## 任务状态
| 任务 | 状态 | 说明 |
| --- | --- | --- |
| F1 两条 Python 合同 | 完成 | 五个归档 token 反向断言只打在 row + management;写入 POST/PATCH 合同保留 |
| F2 `frontend/AGENTS.md` | 完成 | 删符号前 `git grep -- tests/ frontend/` |
| F3 记录 | 完成 | BUG-992;未改 BUG-991 状态 |
| P3 迁移测试从文件切 SQL | 完成 | `retireSql.match` 取出 update 再执行,不再手抄 |
## 改断言三栏(F1
| 测试 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `test_chat_history_management_actions_are_exposed` | `toggleArchivedSession` / `showArchivedSessions` / 「已归档,可在左侧归档中恢复。」 / 「归档」 / 「恢复」必须出现 | 必须不出现,且只对 `sidebar-session-row.tsx``use-session-management.ts` | 归档能力于 2026-09-21 整体下线(BUG-991 |
| `test_archiving_never_calls_the_delete_endpoint` | 切片 `function toggleArchivedSession`,断言归档走 PATCH 不含 DELETE | 改名为 `test_session_archive_is_retired_and_write_helper_still_patches`:归档函数不存在;`writeChatSession` 仍是 `method: mode === "create" ? "POST" : "PATCH"``deleteSession` 切片含 `method: "DELETE"` | 函数没了 `index()` 会抛;D3 写入合同与删除路径不得丢 |
## 测试
| 项 | 结果 |
| --- | --- |
| `python -m pytest tests/test_session_management_entrypoints.py` | 3 条全绿(函数数未减) |
| 反向自测 | `sidebar-session-row.tsx` 临时加「归档」后该测试转红,已撤回未提交 |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error / 119 warning |
| `run_quality_gate.py` | 本 worktree 无 `.venv`,未跑 |
未改前端业务代码。D3 的 `method: mode === "create" ? "POST" : "PATCH"` 仍在 `test_session_archive_is_retired_and_write_helper_still_patches`
+1 -1
View File
@@ -155,7 +155,7 @@
| `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-06BUG-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。删除能力不动 | **验收未通过(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 起 | 待领取 | |
| `TASK-archive-retire-fix-20260921.md` | `PROGRESS-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 起 | 待验收 | `codex/archive-retire-fix-20260921` |
### 个人报告
+11
View File
@@ -3,3 +3,14 @@
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
# Frontend change radius
Before deleting or renaming a frontend symbol, class name, or visible copy, run
`git grep -n "<symbol>" -- tests/ frontend/` — not `frontend/` alone.
`tests/` holds Python source-contract tests that assert on frontend text
(`test_session_management_entrypoints.py`,
`test_daily_and_rectification_entrypoints.py`,
`test_birth_time_journey_contract.py`). Grepping only `frontend/` missed them
four times (BUG-933, BUG-934, BUG-939, BUG-992).
@@ -115,17 +115,15 @@ test("retire archive migration clears archived_at without bumping updated_at", {
);
assert.match(retireSql, /set archived_at = null/);
assert.doesNotMatch(retireSql, /updated_at\s*=/);
fixture.psqlAs("schema_owner", "schema-owner-test-password", `
update public.chat_sessions set archived_at = null where archived_at is not null;
`);
const apply = retireSql.match(/update public\.chat_sessions\s+set archived_at = null\s+where archived_at is not null;/);
assert.ok(apply?.[0], "retire migration must contain the unarchive update");
fixture.psqlAs("schema_owner", "schema-owner-test-password", apply[0]);
assert.equal(fixture.psql("select count(*) from public.chat_sessions where archived_at is not null"), "0");
const after = fixture.psql(
`select pinned::text || '|' || jsonb_array_length(messages)::text || '|' || updated_at::text from public.chat_sessions where id = '${archivedRectification}'`,
);
assert.equal(after, before);
fixture.psqlAs("schema_owner", "schema-owner-test-password", `
update public.chat_sessions set archived_at = null where archived_at is not null;
`);
fixture.psqlAs("schema_owner", "schema-owner-test-password", apply[0]);
assert.equal(fixture.psql("select count(*) from public.chat_sessions where archived_at is not null"), "0");
} finally {
fixture.stop();
+20 -15
View File
@@ -42,9 +42,6 @@ def test_chat_history_management_actions_are_exposed() -> None:
"确认删除",
"session-delete-overlay",
"togglePinnedSession",
"toggleArchivedSession",
"showArchivedSessions",
"已归档,可在左侧归档中恢复。",
"shareSession",
"share_payload_version",
"messages.map",
@@ -54,13 +51,22 @@ def test_chat_history_management_actions_are_exposed() -> None:
"focus-within",
"收藏",
"重命名",
"归档",
"恢复",
"删除",
"转发",
'fetch(`/api/sessions/${encodeURIComponent(session.id)}`',
):
assert expected in source
row = SESSION_ROW.read_text(encoding="utf-8")
management = (_FRONTEND_SRC / "hooks" / "use-session-management.ts").read_text(encoding="utf-8")
for retired in (
"toggleArchivedSession",
"showArchivedSessions",
"已归档,可在左侧归档中恢复。",
"归档",
"恢复",
):
assert retired not in row
assert retired not in management
def test_chat_session_delete_is_server_controlled_and_granted() -> None:
@@ -74,16 +80,15 @@ def test_chat_session_delete_is_server_controlled_and_granted() -> None:
assert 'using ((select auth.uid()) = user_id)' in migration.lower()
def test_archiving_never_calls_the_delete_endpoint() -> None:
def test_session_archive_is_retired_and_write_helper_still_patches() -> None:
source = _home_surface()
write_contract = SESSION_WRITE_CONTRACT.read_text(encoding="utf-8")
start = source.index("function toggleArchivedSession")
end = source.index("async function shareSession", start)
archive_action = source[start:end]
# Former tokens: setArchivedSessionIds and no `/api/sessions/` in the slice.
# Archive is now a metadata PATCH; the write helper uses PATCH for update.
assert 'writeChatSession(sessionId, { archived_at: nextArchivedAt }, "update")' in archive_action
assert "archived_at: nextArchivedAt" in archive_action
assert 'method: "DELETE"' not in archive_action
assert "DELETE" not in archive_action
assert "function toggleArchivedSession" not in source
assert "toggleArchivedView" not in source
assert 'writeChatSession(sessionId, { archived_at: nextArchivedAt }, "update")' not in source
assert "archived_at: nextArchivedAt" not in source
assert 'method: mode === "create" ? "POST" : "PATCH"' in write_contract
start = source.index("async function deleteSession")
end = source.index("function togglePinnedSession", start)
delete_action = source[start:end]
assert 'method: "DELETE"' in delete_action