Keep archive as a PATCH of archived_at rather than DELETE, pass entry_mode through onboarding, and let popstate to the default chat reuse selectSession. Then move send/stop/recovery and session management out of page.tsx so the home surface stays within the batch-two line budget. Co-authored-by: Cursor <cursoragent@cursor.com>
5.5 KiB
PROGRESS · 小刀轮:孤儿契约红修复 + popstate 副作用对齐(2026-09-01)
工作树:.worktrees/contract-repairs-20260901
分支:codex/contract-repairs-20260901(跟踪 origin/staging)
基线:origin/staging @ a6148af8(任务书提交;产品代码与第一批 54269fcf 相同,另加两份任务书)
不要写成 PROGRESS.md。小刀轮与第二批在同一工作树落地(第二批任务书写「小刀轮落地后的 origin/staging」;小刀当时尚未推送,故未另开 home-split-batch2 工作树)。交付为快进推 origin/staging。
开工后 origin/staging 又快进了 4 个无关文档提交(ff97de5e…8b7eda9f),未并入本树。
任务 1 · 归档契约
文件:tests/test_session_management_entrypoints.py::test_archiving_never_calls_the_delete_endpoint
| 项 | 原值 | 新值 |
|---|---|---|
| 读取面 | PAGE.read_text()(仅 page.tsx) |
_home_surface():page.tsx + 第一批抽出文件 + 若存在则拼接 hooks/use-session-management.ts、hooks/use-consultation-run.ts |
| 切片 | function toggleArchivedSession → async function shareSession |
同左(两函数第二批一起搬进 session hook,切片标记仍相邻) |
| 正断言 | "setArchivedSessionIds" in archive_action |
'writeChatSession(sessionId, { archived_at: nextArchivedAt }, "update")' in archive_action 且 "archived_at: nextArchivedAt" in archive_action |
| 负断言 | "/api/sessions/" not in archive_action |
'method: "DELETE"' not in archive_action 且 "DELETE" not in archive_action |
| PATCH | (无) | chat-session-write-contract.ts 含 method: mode === "create" ? "POST" : "PATCH" |
同文件 test_chat_history_management_actions_are_exposed 的读取面从 PAGE + STYLES + SESSION_ROW 改为 _home_surface() + STYLES + SESSION_ROW,断言 token 未改。test_chat_session_delete_is_server_controlled_and_granted 仍读路由/迁移。
CORE_PYTEST_TARGETS:加入 tests/test_session_management_entrypoints.py,注释「Locks archive as PATCH archived_at, never HTTP DELETE (data-safety).」
任务 2 · 引导旅程契约
文件:tests/test_birth_time_journey_contract.py::test_web_onboarding_uses_the_deterministic_free_journey
| 项 | 原值 | 新值 |
|---|---|---|
| page 读取面 | page.tsx + home-profile.ts |
_home_surface()(同上,含可选 hook 文件) |
| mastra 读取面 | frontend/src/mastra/index.ts |
_mastra_consultation_surface() = index.ts + consultation-workflow.ts(index.ts 只再导出;token 在 workflow) |
| 正断言 | 'entry_mode: entryMode' in mastra |
不变(现码 consultation-workflow.ts) |
| 负断言 | 'entry_mode: "direct_chart"' not in mastra |
不变(schema 默认值是 entryMode: z.enum(...).default("direct_chart"),不是 entry_mode: "direct_chart";consultation-tools.ts 未拼进此读取面) |
白名单决定:加入 CORE_PYTEST_TARGETS。整文件只读 SQL 迁移与 TS 源码,无 subprocess、无网络、无 Docker。注释「Pure source/SQL regex for the birth-time journey; no runtime services.」
任务 3 · popstate 副作用对齐
applySessionPopStateRef(现位于 use-session-management.ts,小刀改完后原样搬):
| 项 | 原值 | 新值 |
|---|---|---|
requestedId |
query.present ? query.sessionId : fallbackId(无 ?c= 时直接等于列表首项,空 id 分支实际上只有「列表为空」才会进) |
query.sessionId(无 ?c= 时进空 id 分支) |
| 空 id 分支 | setActiveSessionId(""); return; |
有 fallbackId:sessionSelectionSource.current = "history"; selectSession(fallbackId);;否则维持 setActiveSessionId("") |
有会话时 back 到 / 仍走 selectSession(清草稿/提示、切绑定星盘),来源保持 history,不二次 pushState。无会话时行为不变。
frontend/tests/chat-session-url.test.ts:
- 原 P1 合同「popstate reuses selectSession without a second push」未改断言;切片里
writeSessionUrl(..., "push")仍只有selectSession内那一处。 - 新增「popstate to a missing session query reuses selectSession side effects for the default chat」,锁空 id 分支源码形态,且 popstate 切片内
writeSessionUrl(..., "push")次数为 0。
frontend/tests/home-surface.ts 同步加入可选 hook 文件(existsSync),避免第二批把 selectSession / popstate 搬走后本文件立刻再红。
前瞻(第二批)
两份 Python 合同与 chat-session-url.test.ts 都走拼接读取。第二批把 toggleArchivedSession / applySessionPopStateRef 搬进 use-session-management.ts 后,hook 文件进可选清单即可。切片标记保持相邻。
验证
./node_modules/.bin/tsc --noEmit:通过(小刀后与第二批抽取后各跑一次)。python3.12 -m pytest tests/test_session_management_entrypoints.py tests/test_birth_time_journey_contract.py -q:12 passed(3 + 9)。- 连同
tests/test_supabase_user_data_contract.pytests/test_daily_and_rectification_entrypoints.py:24 passed。 tsx --test tests/chat-session-url.test.ts:12 pass / 0 fail / 0 skipped。- 前端全量
npm test:2428 pass / 0 fail / 0 skipped(小刀新增 1 条 popstate 源码合同;相对第一批 2427)。 ./node_modules/.bin/next build --webpack:┌ ○ /Static。首屏 JS gzip-9 = 501104 B(第一批后 501315 B,−0.04%)。
未改 .gitea/workflows/**。未动数据库。未提升 main。