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

196 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 是归档分支那一半断言:
```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」的已知代价,
若产品不接受再另立单。