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
196 lines
11 KiB
Markdown
196 lines
11 KiB
Markdown
# 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」的已知代价,
|
||
若产品不接受再另立单。
|