Files
Jyotisha/docs/tasks/TASK-session-list-rebuild-fix-20260921.md
T
Jesse_ChenandClaude Opus 5 70d595e361 docs(tasks): 会话列表重建验收结论与修复单(BUG-990)
e71e4f92 三条实现均通过独立复跑;门禁 run 2832 红在兼容层
not(col,"is",null) 未实现,归档视图自 a1956deb 起 500。

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

11 KiB
Raw Blame History

TASK 会话列表重建 · 验收修复单:兼容层不支持 not(col,"is",null),归档视图 500 且门禁红

  • 日期:2026-09-21
  • 基线 commit:e71e4f92(origin/staging head;该提交未部署,staging 仍停在 0c3c9d6b)
  • 分支:codex/session-list-rebuild-fix-20260921
  • BUG 编号起点:BUG-990(开工核对 docs/BUG_HISTORY.md 实际最大号,当前最大 BUG-989)
  • 前序:TASK-session-list-rebuild-20260921.md / PROGRESS-session-list-rebuild-20260921.md
  • 关联记录:BUG-553、BUG-926、BUG-987

1. 事故实证

Gitea backend-quality-gate run 2832(e71e4f92,2026-09-21 17:06→17:23):validate job 第 6 步 Validate backend, package, frontend, and database contracts 失败,publish 被跳过,因此 staging 至今没有部署这一版——BUG-987/988/989 三条修复一条都没到用户面前。

门禁机有 Docker,测试总数 3651,3650 通过 / 1 失败 / 0 skip。唯一的红是本轮新加的那条真实 Postgres 测试:

not ok 1238 - session list query keeps empty rectification rows and drops empty consultations
  error: 'unsupported not filter'
  stack: |-
    LocalPostgresQueryBuilder.not (frontend/src/lib/db/local-postgres-client-core.ts:391:34)
    applyArchiveFilter (frontend/src/lib/session-list-filter.ts:18:27)
    TestContext.<anonymous> (frontend/tests/database-session-list-visibility.test.ts:62:7)

database-session-list-visibility.test.ts:62 是归档分支那一半断言:

const archived = await excludeEmptyConsultations(
  applyArchiveFilter(local.from("chat_sessions").select("id")…, true),
);

applyArchiveFilter(query, true) 调 query.not("archived_at", "is", null)。本地 Postgres 兼容层的 LocalPostgresQueryBuilder.not()(local-postgres-client-core.ts,符号定位 not(column: string, operator: string, value: unknown))只认两个算子:

if (operator === "eq") { …neq… ; return this; }
if (operator !== "cs") throw new Error("unsupported not filter");

"is" 直接 throw。

1.1 这不只是测试问题:归档视图在本地 PG 上一直是 500

applyArchiveFilter 在 GET /api/sessions 的 try 块里同步执行,抛出后落到外层 catch, 返回 500 聊天记录暂时无法读取。而侧栏确实有归档入口(app-sidebar.tsx 的 归档记录 分组、 SidebarSessionControls.onToggleArchivedView),切过去会调 fetchSessions(…, { archived: true }) → /api/sessions?archived=1 → 500。

git log -S'archived_at", "is", null' 显示这行由 a1956deb(2026-09-06,BUG-553 会话列表分页) 引入。 也就是说归档视图从 09-06 起就打不开,两周没被发现,因为当时没有任何真实数据库测试走这条路径, 而源码正则合同只看得见"有没有写这行"。本轮新测试是第一次真跑它,于是把它照出来了。

托管 Supabase 路径不受影响(PostgREST 原生支持 not.is.null),但生产与 staging 都跑自托管本地 PG。


2. 根因

兼容层 not() 实现的是 PostgREST 算子的一个子集(只有 eq / cs),调用方却按完整 PostgREST 语义写。 这是 BUG-926「order() 只保留最后一键」的同一类缺陷:兼容层缺口不会在构造期报错,只在真跑到那条 SQL 时才炸, 而全仓只有源码正则合同在看这些调用点。


3. 决策记录

  • D1:在兼容层补 not(column, "is", null | true | false),映射成 is not null / is not true / is not false。 这是正解——not.is.null 是 PostgREST 标准写法,调用方没写错。不得反过来改 applyArchiveFilter 去绕开它, 那只会把缺口留给下一个调用方。
  • D2:本轮不得删改 database-session-list-visibility.test.ts 的任何断言来让门禁变绿。它抓到的是真 Bug。
  • D3:归档视图 500 作为独立 Bug 记录(BUG-990),并写明复发自 BUG-926 的同一类兼容层缺口。

4. 硬红线

  1. 不得通过弱化或删除测试断言让门禁通过。
  2. 兼容层任何新增算子必须有真实 Postgres 覆盖,不得只加源码正则。
  3. 不得顺手改 excludeEmptyConsultations / 副标题 / 新建落库这三条已验收通过的逻辑(见 §6 验收结论)。
  4. 不得顺手升级依赖、不得顺手修不在本单里的 warning。
  5. frontend/src/app/(app)/page.tsx 的 useState / useRef 数不得增长(AGENTS.md §6);本单预期不碰这个文件。

5. 任务分解

F1|兼容层支持 not(col, "is", value)(BUG-990)

  • LocalPostgresQueryBuilder.not() 增加 operator === "is" 分支,新增一种 filter kind(例如 isNot), 在 filterClause() 里编译成 ${column} is not null / is not true / is not false; 其余取值仍 throw(与既有 is 分支的 unsupported is filter 保持一致的失败语义)。
  • 不要把它塞进现有 neq 分支:archived_at <> null 恒为 NULL,会静默返回 0 行——比 throw 更糟。
  • 验收标准:
    • frontend/tests/database-session-list-visibility.test.ts 在有 Docker 的机器上整条通过,归档断言返回且只返回那条已归档的校正会话。
    • 新增单元覆盖:not(col,"is",null) 编译出的 SQL 含 is not null 且不含 <>;not(col,"is",undefined) 仍 throw。
    • local-postgres-or.test.ts / chat-session-authority.test.ts 不变仍绿。

F2|归档视图回归证据(BUG-990)

  • 在同一个真实 Postgres 测试里补一条:归档视图不返回未归档行、也不返回空咨询(已归档的空咨询仍不列出)。 现有测试已覆盖前者,把"已归档的空咨询"这一行加进 fixture 断言即可。
  • 验收标准: 归档与正常两个视图的返回集合互斥且都符合 cloudListIncludesSession 的六种组合。

F3|记录(BUG-990)

  • docs/BUG_HISTORY.md 新增 BUG-990:现象写「侧栏切到归档记录后 500」,根因写兼容层 not 算子子集, 复发自 BUG-926(同一兼容层的不同缺口),防复发写「兼容层新增算子必须有真实 Postgres 覆盖」。
  • CHANGELOG.md 补一句用户可感知的:归档记录可以打开了。
  • docs/tasks/README.md 状态板更新本单与前序单。
  • 前序单 BUG-987/988/989 的「修复版本」在本单合入并部署后回填实际 SHA。

6. 前序单(e71e4f92)的验收结论——除本单外全部通过,不要重做

我在 .worktrees/verify-20260921 用 f8d65e48 作基线独立复跑:

项 基线 f8d65e48 e71e4f92 结论
tsc --noEmit — 0 错 通过
npm run lint 0 error / 119 warning 0 error / 119 warning 通过,warning 未增
npm test(本机无 Docker) 3634 条 / 3580 过 / 34 红 / 20 skip 3640 条 / 3585 过 / 34 红 / 21 skip 失败清单 diff 逐条一致;净增 5 绿 + 1 skip(即那条 Docker 测试)
next build --webpack 通过 通过 / 仍 ○ Static
首屏 gzip(html + 29 个首屏 js/css,level 9) 643,288 643,115 −173 B / −0.027%
page.tsx 行数 1826 1791 缩小

逐条对任务书:

  • T1 BUG-987 通过(实现正确,被 F1 挡住):过滤收窄成 or("session_type.neq.consultation,messages.neq.[]"),抽到 session-list-filter.ts 与客户端同文件; 错误注释已删。跨层对断测试覆盖六种组合。归档分支的规则写对了,跑不起来是兼容层的锅。
  • T2 BUG-988 通过:副标题改 updatedAt,SESSION_LIST_COLUMNS 去掉 created_at, session-groups.test.ts 新增的断言正是截图那条(9/7 创建、9/16 活动)——排在活动时间该在的位置、副标题显示活动时间。
  • T3 BUG-989 通过:startNewChat 不再 POST、不再写 ?c=;send() 在扣点之后、乐观写入之前 persistSession(currentSession, "create"),失败撤销本地会话且发生在 setDraft("") 之前(草稿不丢); cloudCreatedIds 防重复 POST;启动落点与 draft / draftRow / findReusableEmptyConsultation 全部删除。
  • 记录:BLOCKED.md 第 120 行那条按 §4 划掉而非删除,解除说明写明了 BUG-989/987。

7. 交付前必须全跑

  • cd frontend && ./node_modules/.bin/tsc --noEmit → 0 错
  • npm run lint → 0 error,warning 不得超过 119
  • npm test → 全量;失败清单与 e71e4f92 逐条比对
  • npm run test:db → 本单必跑,这是 F1 的唯一真证据;确实没有 Docker 时按 §8 让步
  • npm run build → / 仍 Static,首屏 gzip 与 e71e4f92 相差 ±2% 内

8. 让步顺序

  1. 无 Docker → test:db 跑不了:F1 的实现与测试照写照提交,在 BLOCKED.md 记明, 并以「门禁 run 编号 + 该测试转绿」作为交付证据——本单允许把最终验证交给门禁机, 但不得在 test:db 未绿的情况下把 BUG-990 写成 resolved。
  2. 其它一律不让步:F1 是唯一挡住三条已完成修复上线的东西。

9. 开工前置命令

cd /workspace/Jyotisha
git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/session-list-rebuild-fix-20260921 \
  .worktrees/session-list-rebuild-fix-20260921 origin/staging
cd .worktrees/session-list-rebuild-fix-20260921/frontend
ln -s /workspace/Jyotisha/frontend/node_modules node_modules   # 或 npm ci
./node_modules/.bin/tsc --noEmit
npm test 2>&1 | tail -20        # 记录基线(本机 3640 / 34 红 / 21 skip)

开工必读:docs/BUG_HISTORY.md 的 BUG-926(兼容层 order() 缺口,同类)、BUG-553、BUG-987; docs/tasks/PROGRESS-session-list-rebuild-20260921.md;AGENTS.md §2、§3、§5、§7。

纯前端 + 兼容层,不要求 scripts/pre_work_check.py。


10. 真实环境欠账(本单不负责,部署后由产品负责人走)

e71e4f92 + 本单部署到 staging 后需要人工确认,建议写进 docs/testing/:

  1. 侧栏能看到 09-08 以来的生时校正会话,且时间从上到下单调递减。
  2. 截图里那条「9月16日 · 今日节奏」现在显示 9月16日(而不是 9月7日)。
  3. 连点五次「新建对话」,侧栏不出现任何空「新对话」;发一句话后出现一行,时间是今天。
  4. 第一句话发送失败时,输入框里的字还在。
  5. 切到「归档记录」不再报错(F1 的用户侧证据)。
  6. 新建后地址栏仍是上一条会话的 ?c=,刷新会回到上一条——这是「未落库不写 URL」的已知代价, 若产品不接受再另立单。