# 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. (frontend/tests/database-session-list-visibility.test.ts:62:7) ``` `database-session-list-visibility.test.ts:62` 是归档分支那一半断言: ```ts 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)`)只认两个算子: ```ts 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. 开工前置命令 ```bash 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」的已知代价, 若产品不接受再另立单。