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
11 KiB
TASK 会话列表重建 · 验收修复单:兼容层不支持 not(col,"is",null),归档视图 500 且门禁红
- 日期:2026-09-21
- 基线 commit:
e71e4f92(origin/staginghead;该提交未部署,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. 硬红线
- 不得通过弱化或删除测试断言让门禁通过。
- 兼容层任何新增算子必须有真实 Postgres 覆盖,不得只加源码正则。
- 不得顺手改
excludeEmptyConsultations/ 副标题 / 新建落库这三条已验收通过的逻辑(见 §6 验收结论)。 - 不得顺手升级依赖、不得顺手修不在本单里的 warning。
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 不得超过 119npm test→ 全量;失败清单与e71e4f92逐条比对npm run test:db→ 本单必跑,这是 F1 的唯一真证据;确实没有 Docker 时按 §8 让步npm run build→/仍 Static,首屏 gzip 与e71e4f92相差 ±2% 内
8. 让步顺序
- 无 Docker →
test:db跑不了:F1 的实现与测试照写照提交,在BLOCKED.md记明, 并以「门禁 run 编号 + 该测试转绿」作为交付证据——本单允许把最终验证交给门禁机, 但不得在test:db未绿的情况下把 BUG-990 写成resolved。 - 其它一律不让步: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/:
- 侧栏能看到 09-08 以来的生时校正会话,且时间从上到下单调递减。
- 截图里那条「9月16日 · 今日节奏」现在显示 9月16日(而不是 9月7日)。
- 连点五次「新建对话」,侧栏不出现任何空「新对话」;发一句话后出现一行,时间是今天。
- 第一句话发送失败时,输入框里的字还在。
- 切到「归档记录」不再报错(F1 的用户侧证据)。
- 新建后地址栏仍是上一条会话的
?c=,刷新会回到上一条——这是「未落库不写 URL」的已知代价, 若产品不接受再另立单。