fix(sessions): title rectification chats with the result time

This commit is contained in:
jesse-ux
2026-09-25 01:56:57 +08:00
parent 514951d708
commit 88fe67df9b
17 changed files with 592 additions and 118 deletions
+5
View File
@@ -1,5 +1,10 @@
# 印度占星 Skill 更新日志
## 2026-09-25 — 校正会话标题改为结果时间
- 历史校正会话的名字改成显示算出来的出生时间或当前范围,不再在标题里重复日期。
- 今日节奏仍带日期。手改过的名字不动。Skill 版本不 bump。
## 2026-09-24 — 报告操作收敛与桌面导出居中(本地未提交,依赖验收 blocked)
- 报告列表已完成行只保留「查看报告」;阅读页仅保留返回和导出图标,不再自动消费旧 `#export` 链接。
+16
View File
@@ -13603,3 +13603,19 @@
- 相关记录:BUG-999/1003;TASK-report-chapter-export-fix-20260924.md。
- 复发自:BUG-999同输出边界的未覆盖换行路径,继承剥离器缺陷,非本轮新增回归。
- 修复版本:本地待提交。
## BUG-1001 | 校正会话标题仍写日期,同一天多条无法区分
- 状态:resolved
- 首次发现:2026-09-22
- 最近更新:2026-09-25
- 影响面:侧栏校正会话标题、`resolveSessionTitle`、`agentic_rectification_cases` 的结果列、存量 `chat_sessions.title`
- 用户现象:副标题已经是最后活动时间,标题仍印日期,同一天多条校正完全同名;旧标题里的钟点是创建时间,和副标题对不上。
- 触发条件:侧栏列出多条生时校正;或校正结果(已确认、已采用、当前范围)变化后标题仍是日期。
- 根因:复发自 BUG-929。BUG-988 把副标题改成最后活动时间之后,标题继续承载日期,同一行两列是同一个事实,而且日期不够区分同日多条。BUG-929 当时决定旧标题不批量改。
- 修复:标题算式只有 `public.rectification_session_title`。已确认优先于已采用,写 `生时校正 · HH:MM`;否则起止都通过 `agentic_rectification_is_clock` 时写 `生时校正 · HH:MM–HH:MM`(U+2013);否则 `生时校正`。触发器 `agentic_rectification_cases_title_from_result` 只挂在 `accepted_time`、`confirmed_time`、`candidate_range`。回填与触发器共用 `rectification_session_title_is_automatic`,只覆盖自动派生标题。不写 `updated_at`,不改 pinned、messages。今日节奏仍带日期。open 路径未改。
- 验证:`database-rectification-session-title-result.test.ts` 在本机真实 Postgres 通过:四分支、`provolatile = i`、采用分钟与区间收窄不改 `updated_at`、手改「我的校正」不被覆盖、touch 触发器仍推进活动时间、从迁移文件切出的回填改写前五条、第六条不动、六行 `updated_at` / `pinned` / `messages` 逐字节不变、重跑 0 行。跨层对断测试读迁移里的正则,与 `isAutoDerivedSessionTitle` 同组输入答案相同;把该正则的间隔号改成连字符后测试转红,已撤回。
- 防复发:标题算式只能有一处实现,运行时与回填共用。侧栏同一行的两列不得承载同一个事实。
- 相关记录:BUG-929、BUG-699、BUG-704、BUG-988、BUG-987、BUG-992、BUG-995
- 复发自:BUG-929(标题口径两次返工;「旧标题不批量改」被本单推翻)
- 修复版本:本分支提交,尚未推送、尚未部署
@@ -0,0 +1,55 @@
# PROGRESS 校正会话标题改为结果时间(2026-09-25)
- 任务书:`docs/tasks/TASK-rectification-session-title-result-20260922.md`
- 分支:`codex/rectification-session-title-result-20260922`
- 基线:`514951d7`(开工时 `origin/staging`)
- 状态:实现完成,待验收。未 push。
## 做了什么
- 新增 `frontend/supabase/migrations/20260925010000_rectification_session_title_result.sql`。没有抄到 `frontend/db/migrations`(校正迁移只在 supabase,runner 会扫该目录)。
- 标题算式只在 `public.rectification_session_title(time, time, jsonb)`。已确认优先于已采用;范围原样拼接,中间是 U+2013 EN DASH;否则 `生时校正`。`to_char(..., 'HH24:MI')`。`immutable` / `parallel safe` / `search_path = ''`,不读表。
- 自动派生判据只在 `public.rectification_session_title_is_automatic(text)`。触发器与回填都调它。
- 触发器 `agentic_rectification_cases_title_from_result`,函数 `public.sync_rectification_session_title_from_case()`。只挂 `accepted_time`、`confirmed_time`、`candidate_range`。不写 `updated_at`。没有改 `touch_chat_session_from_rectification_case`。
- 回填 update 用同一对函数。测试从迁移文件的标记之间切出该语句执行,没有手抄。
- `resolveSessionTitle` 对校正入口和校正交接问句返回 `生时校正`。`datedSessionTitle` 只剩今日节奏调用者,注释写明。
- `DATED_ENTRY_TITLE` 扩到三种自动派生形态,并认得今日节奏的日期形态。连字符范围不是自动派生。
- 删了 `frontend/src/lib/rectification-session-title-repair.ts` 和 `frontend/tests/rectification-session-title-repair.test.ts`。它们只服务已被本单取代的「按创建日把标题改回日期」助手,生产代码没有引用。历史脚本 `frontend/scripts/repair-rectification-session-titles.mjs` 仍被 `rectification-session-title-repair-migration.test.ts` 用来锁 `20260916020000` 的逐字合同,`--apply` 本来就会报错,没有改成新口径,也没有改历史迁移。脚本头注明不得执行 `TITLE_APPLY_SQL`。
- open 路径一个字没动(D4 / BUG-699)。
## 断言三栏
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| `resolveSessionTitle` 校正入口 `生时校正 · 8月22日` | `生时校正` | BUG-1001:新建不带日期,结果由触发器写;今日节奏仍是 `今日节奏 · 8月22日` |
| `session-list-filter` 夹具标题 `生时校正 · 9月14日` | `生时校正 · 05:07` | 例子改成结果时间。该断言只锁是否入列,不锁标题格式 |
| `shouldGenerateSessionTitle("生时校正", consultation)` 为 true(经 generic 前缀,未单测) | false | `DATED_ENTRY_TITLE` 现在认得裸「生时校正」和结果时间,模型不得覆盖自动标题。校正会话仍恒 false |
删掉的旧修补测试(任务书允许,T5 已取代):`dated titles use the product calendar, not UTC`、`repair rewrites a wall-clock today stamp back to created_at's local date`、`repair leaves matching dates and user-renamed titles alone`、`missing created_at falls back to a dateless title instead of guessing`。
## T4 故意改红
把迁移里 `rectification_session_title_is_automatic` 正则的范围间隔号 U+2013 改成连字符 `-`(一个字符)。`rectification-session-title-parity.test.ts` 转红(`not ok`,正则不再含 EN DASH)。已撤回,复跑 `ok`。未提交该改动。
## 验证
| 项 | 结果 |
| --- | --- |
| `npx tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error,121 warning。本单改动的文件没有新增 warning。121 是当前树上的既有数,超过任务书 119 的上限,按「不修无关 warning」没有动 |
| 定向:`agent-reply`、`session-title-agent`、`rectification-session-title-parity`、`rectification-session-title-repair-migration`、`session-list-filter` | 23/23 通过 |
| `database-rectification-session-title-result.test.ts` | 真实 Postgres 1/1 通过(约 30s)。四分支、`provolatile=i`、采用/收窄不改 `updated_at`、手改名不覆盖、touch 与标题触发器互不干扰、回填 5 行、重跑 0 行、六行 `updated_at`/`pinned`/`messages` 不变。套在 `npm run test:db` 里仍是 `ok 41` |
| `npm run test:db` | 跑完,exit 1。48 条里 13 过、35 红。红的是既有 Windows 缺口:双目录扫描 `duplicate migration filename: 20260814025000_personal_report_document_v2.sql`,以及 backup 测试的 WSL `execvpe(/bin/bash)`。不是本单 SQL。地址池没有耗尽 |
| 全量 `npm test` | 未跑。这台 Windows 有已知 Docker/EPERM 基线失败,定向已绿 |
| `npm run build` | 失败。Turbopack 报 worktree `node_modules` junction 指向文件系统根之外。`npx next build --webpack` 编译成功(69s),收集页面时在 `/api/daily-starlanguage` 与 `/api/rectification/agent` 因 Skill symlink EPERM 中断,没有打出路由表。`/` 是否仍 Static、首屏 gzip 本机测不到。这是既有 Windows 缺口,不是本单源码错误 |
| Python `tests/test_daily_and_rectification_entrypoints.py` | 未跑。`G:\Ferti\Jyotisha\.venv\Scripts\python.exe` 不存在,没有装依赖。开工已看过该文件,只断言裸字符串「生时校正」,新格式仍满足 |
Windows 上 `frontend/db/migrations` 里 4 个 Git symlink 被检出成相对路径文本,标准双目录扫描报 `duplicate migration filename: 20260814025000_personal_report_document_v2.sql`。定向数据库测试先走标准 runner;只有撞上这个既有错误时,才把真正的 SQL 逐字节抄到临时目录(跳过占位文本,重复真文件必须一致),再用 `MIGRATIONS_DIRECTORY` 跑同一个 `scripts/db-migrate.mjs`。Linux 门禁 symlink 不是普通文件,第一条路径就会成功。没有改 runner。
## 未改 / 未验证
- 没有 push,没有改 workflow,没有动 `page.tsx`、咨询 status 路由、SVG height。
- open 之后客户端仍会 `persistSession` 写本地 minted 标题「生时校正」。任务书要求 open 路径不动。插入触发器先写成范围标题,这次 PATCH 可能把它写回「生时校正」,直到下一次 `accepted_time` / `confirmed_time` / `candidate_range` 变化再由触发器写回。刷新后的列表读的是库里的标题;已加载列表再打开时,本地标题与库一致,不会改回去。
- 连字符范围 `生时校正 · 05:00-05:15`:SQL 不覆盖。`isAutoDerivedSessionTitle` 仍可能为 true,因为 `isGenericSessionTitle` 的「生时校正」前缀比本单三种形态更宽。对断表没有把这一条算进「必须相同」的那一组。`DATED_ENTRY_TITLE` 不认连字符。
- 首屏 gzip 基线未测到。`npm run build` 在 junction 上 Turbopack panic;`next build --webpack` 编译过了,但页面收集被 Skill symlink EPERM 打断,没有路由表,不能宣称 `/` 仍是 Static。
- 浏览器走查未做。
+1 -1
View File
@@ -33,7 +33,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-rectification-session-title-result-20260922.md` | — | **校正会话标题改写结果(BUG-1001)**:BUG-988 把副标题改成最后活动时间后,标题里的日期成了重复;而 BUG-929 删掉 `uniquifySessionTitle` 之后,`resolveSessionTitle` 对校正入口只返回 `生时校正 · M月D日`,**同一天多条标题完全相同**(真机截图:9/17 三条同名、9/16 两条同名、9/14 一天 5 条);旧标题的钟点后缀是创建时间、副标题是最后活动时间,同一行两个对不上的时间。**产品 09-22 拍板**:D1 标题改为承载结果——有 `accepted_time`/`confirmed_time` 写 `生时校正 · HH:MM`,否则写 `candidate_range` 的 `生时校正 · HH:MM–HH:MM`(推翻 BUG-929 的日期口径,但「不得再加墙钟去重后缀」保留);D2 存量批量重算(推翻 BUG-929「旧标题不批量改」,先例 `20260916020000_rectification_session_title_repair.sql`);D3 今日节奏保留日期(我判定的例外);D4 只在结果变化时改 title,`open` 路径不动(对 BUG-699 边界的有限扩展)。**红线**:任何写标题的路径都不得 bump `updated_at`(否则 13 条历史会话集体跳顶、毁掉 BUG-988);标题算式必须只有一处实现(一个 SQL 函数,触发器与回填共用)——BUG-987/992 都栽在「两层规则各写一份」。T4 跨层对断不得让步。已 grep 出真正受影响的只有 7 个文件,Python 侧只查裸字符串、无需改。BUG 段 1001 起 | 待领取 | — |
| `TASK-rectification-session-title-result-20260922.md` | `PROGRESS-rectification-session-title-result-20260922.md` | **校正会话标题改写结果(BUG-1001)**:BUG-988 把副标题改成最后活动时间后,标题里的日期成了重复;而 BUG-929 删掉 `uniquifySessionTitle` 之后,`resolveSessionTitle` 对校正入口只返回 `生时校正 · M月D日`,**同一天多条标题完全相同**(真机截图:9/17 三条同名、9/16 两条同名、9/14 一天 5 条);旧标题的钟点后缀是创建时间、副标题是最后活动时间,同一行两个对不上的时间。**产品 09-22 拍板**:D1 标题改为承载结果——有 `accepted_time`/`confirmed_time` 写 `生时校正 · HH:MM`,否则写 `candidate_range` 的 `生时校正 · HH:MM–HH:MM`(推翻 BUG-929 的日期口径,但「不得再加墙钟去重后缀」保留);D2 存量批量重算(推翻 BUG-929「旧标题不批量改」,先例 `20260916020000_rectification_session_title_repair.sql`);D3 今日节奏保留日期(我判定的例外);D4 只在结果变化时改 title,`open` 路径不动(对 BUG-699 边界的有限扩展)。**红线**:任何写标题的路径都不得 bump `updated_at`(否则 13 条历史会话集体跳顶、毁掉 BUG-988);标题算式必须只有一处实现(一个 SQL 函数,触发器与回填共用)——BUG-987/992 都栽在「两层规则各写一份」。T4 跨层对断不得让步。已 grep 出真正受影响的只有 7 个文件,Python 侧只查裸字符串、无需改。BUG 段 1001 起 | 待验收 | `codex/rectification-session-title-result-20260922` |
| `TASK-rectification-convergence-20260830.md` | `PROGRESS-rectification-convergence-20260830.md` | 收敛重构 v2 | 已验收 | `3a4396a4`、`86ba17ee` |
| `TASK-rectification-decision-authority-20260831.md` | `PROGRESS-rectification-decision-authority-20260831.md` | 决策权威归一与停止语义 | 已合入 | `9f011194` |
| `TASK-rectification-billing-20260831.md` | — | run_failed 与开场问题槽为空 | 已合入 | `47b4b06b` |
+1 -1
View File
@@ -362,7 +362,7 @@ The birth-time rectification session is the consultation transcript plus a house
### Navigation item
- **Structure:** title is the session name. New dated titles are `生时校正 · M月D日` / `今日节奏 · M月D日` (category first). Subtitle is always `M月D日 HH:MM` in Asia/Shanghai from `updated_at` (last activity), plus ` · <chart name>` when the chart is not the account holder. Same-day duplicates are not uniquified with wall-clock `HH:MM`. Display, sort, grouping, and cursor all use `updated_at`.
- **Structure:** title is the session name. Rectification titles show the result: `生时校正 · HH:MM` when a time is confirmed (confirmed wins over accepted) or only accepted, `生时校正 · HH:MM–HH:MM` (en dash) when only `candidate_range` is a clock pair, otherwise `生时校正`. Daily rhythm stays `今日节奏 · M月D日`. Subtitle is always `M月D日 HH:MM` in Asia/Shanghai from `updated_at` (last activity), plus ` · <chart name>` when the chart is not the account holder. Same-day duplicates are not uniquified with wall-clock `HH:MM`. Display, sort, grouping, and cursor all use `updated_at`.
- **States:** default, hover, current, focus, disabled.
- **Hierarchy:** section labels stay tertiary; session titles and primary actions use ink so history rows do not collapse into the same gray as “收藏对话 / 历史对话”. History groups use the overline token for “今天 / 昨天 / 最近 7 天 / 最近 30 天 / 更早”. When the chart is not the account holder, a secondary line shows the chart name under the title. A session title is named when it is created; opening a stored session must not rename it. `updatedAt` advances only on conversation activity, including rectification turns, choice, adopt, and stop. Opening, refresh, and metadata PATCH do not bump it. A `?c=` that is not on the loaded page is looked up with `GET /api/sessions/{id}` before anyone may say it was deleted. Locked by `frontend/tests/session-open-preserves-identity.test.ts` and `frontend/tests/session-lookup-unlisted.test.ts`.
- **Request behavior:** existing sessions remain selectable for reading while a request is active; creating or sending another request stays locked until the active request settles.
+1 -1
View File
@@ -99,7 +99,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
| Agent 未完成必要的方法与计算步骤,本次不会扣点。(模型其实写过一段) | 把模型写过的那段留下,文末由服务端加一句:「这次没跑完星盘计算,上面是模型直接写的,先看着。」 | 合同没绿但写过字时不得整轮空白。这句不是模型写的。没写过字仍报未完成、不扣点。 |
| 这一页是天象本身,不是对你的判断。 | (不写) | 星历页只放日期、五要素、行运、九十天事件。「带这天去提问」在顶栏。不另印定性句。 |
| (校正会话上普通输入框仍可打字) | 正在打开生时校正… | 校正会话上普通输入框只有禁用态。打开失败时按钮写「重新打开生时校正」。 |
| 9月14日 · 生时校正 / 9月14日 · 生时校正 02:14 | 生时校正 · 9月14日 | 新建校正/今日节奏标题类别在前、日期在后。同日多条不再加墙钟时刻,靠副标题 `M月D日 HH:MM` 区分。旧标题不批量改。 |
| 9月14日 · 生时校正 / 生时校正 · 9月14日 / 生时校正 · 09:35(创建钟点) | 生时校正 · 05:07 / 生时校正 · 05:00–05:15 / 生时校正 | 校正标题写结果:已确认优先于已采用,写 `生时校正 · HH:MM`;否则当前范围写 `生时校正 · HH:MM–HH:MM`(间隔号,不是连字符);都没有则只写「生时校正」。今日节奏仍是 `今日节奏 · M月D日`。同日不再用墙钟去重。副标题仍是最后活动时间。旧标题按结果批量改;手改过的名字不动。 |
| 这么晚还醒着,{名字}。直接说说最在意的问题吧。 | 还没睡呀,{名字} | 开场只一行、不追问、不写场景;提问交给输入框占位符 |
| 你更适合把已经积累的专业能力做成长期事业,而不是频繁换赛道。眼下更值得把现有工作做深,而不是立刻另起一摊。 | 今年你的事业是「外松内紧」:表面上木星走到你的事业宫,机会、人缘都在,看着轻松;底下土星压着你的工作宫,做事的规矩和身体的负担在收紧。推动这一年的是土星,不是木星,木星只负责把结果修得体面。所以别去应木星的「享受」,也别去应土星的「磨」,去扮演土星的「守」:把规矩立起来,别人松你不松。<br>- 上半年别接口头承诺,只接写下来的。<br>- 6 月前身体的小毛病别拖。<br>- 9 月木星换宫之后再谈升职,之前谈是白谈。 | 坏的那句像顾问报告:没有反差,没说谁在推谁在修,没有可扮演的象。好的那段是拍板的口气定稿(盘上数据是示意):反差 → 谁推谁修 → 扮演哪个象 → 最多三条短行动。 |
| (用户说「你在说什么鬼」)抱歉让你困惑了,我再解释一下:从星盘来看,您的事业宫…… | 说直接点:这份工作还值得留,但你得换个位置,不是换公司。哪句没看懂,指给我。 | 追问轮不重开一遍报告,也不道歉铺垫。一句白话结论 + 一句把球踢回去。 |
@@ -1,12 +1,14 @@
// 只读核对工具。**修补本身已由迁移承担**:
// 历史核对工具,不是活的改标题路径。--apply 会直接报错。
// 下面三段 SQL 只为锁住历史迁移
// frontend/supabase/migrations/20260916020000_rectification_session_title_repair.sql
// 产品跑 Gitea → `Migrate Staging Database` 就会应用它,不需要 SSH 或库口令。
// 不得执行 TITLE_APPLY_SQL:它会把标题写回日期。
// 结果时间口径的回填是
// frontend/supabase/migrations/20260925010000_rectification_session_title_result.sql。
//
// 本文件保留下来有两个用处:
// 1. 排查时数一数还有多少行没对上(不写库,只 select count);
// 2. 下面三段 SQL 是那条迁移的**唯一出处**,迁移逐字抄它们。
// tests/rectification-session-title-repair-migration.test.ts 会比对两边,
// 改了这里而没同步迁移(或反过来)测试就红。
// 2. 下面三段 SQL 只锁历史迁移的逐字合同,不是新的改标题路径。
// tests/rectification-session-title-repair-migration.test.ts 会比对两边。
//
// 需要 SCHEMA_DATABASE_URL 才能连库;没有它的环境只能跑上面那个合同测试。
+3 -1
View File
@@ -81,6 +81,8 @@ function clipTitle(value: string, maxChars = 14): string {
return characters.length > maxChars ? `${characters.slice(0, maxChars).join("")}…` : value;
}
// Daily rhythm only. Rectification titles are the result time, written by
// public.rectification_session_title when the case result changes (BUG-1001 / D3).
function datedSessionTitle(at: Date, category: string): string {
const { month, day } = shanghaiDateParts(at);
return `${category} · ${month}月${day}日`;
@@ -99,7 +101,7 @@ export function resolveSessionTitle(
return datedSessionTitle(at, "今日节奏");
}
if (options.entrypoint === "birth_time_rectification" || isRectificationHandoffQuestion(question)) {
return datedSessionTitle(at, "生时校正");
return "生时校正";
}
const normalized = question.replace(/\s+/g, " ").trim().replace(/[??!!。.,,;;::]+$/u, "");
if (!normalized) return "新对话";
@@ -1,43 +0,0 @@
/** Product UI dates are wall-clock in China. Repair uses this zone, never UTC. */
export const RECTIFICATION_TITLE_TIMEZONE = "Asia/Shanghai";
export const RECTIFICATION_DATED_TITLE =
/^(\d{1,2})月(\d{1,2})日\s*·\s*生时校正(?:\s+\d{2}:\d{2})?$/;
export function datedRectificationTitle(
createdAt: Date,
timeZone = RECTIFICATION_TITLE_TIMEZONE,
): string {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
month: "numeric",
day: "numeric",
}).formatToParts(createdAt);
const month = Number(parts.find((part) => part.type === "month")?.value);
const day = Number(parts.find((part) => part.type === "day")?.value);
if (!Number.isInteger(month) || !Number.isInteger(day)) {
return "生时校正";
}
return `${month}月${day}日 · 生时校正`;
}
export function repairedRectificationTitle(row: {
title: string;
createdAt: Date | null;
}): string | null {
const matched = row.title.match(RECTIFICATION_DATED_TITLE);
if (!matched) return null;
if (!row.createdAt || Number.isNaN(row.createdAt.getTime())) {
return "生时校正";
}
const next = datedRectificationTitle(row.createdAt);
const nextMatch = next.match(RECTIFICATION_DATED_TITLE);
if (
nextMatch
&& Number(matched[1]) === Number(nextMatch[1])
&& Number(matched[2]) === Number(nextMatch[2])
) {
return null;
}
return next;
}
+4 -1
View File
@@ -10,7 +10,10 @@ function clipHan(value: string, maxChars: number): string {
}
const BIRTH_STAMP = /\d{4}年\d{1,2}月(?:\d{1,2}日)?|\d{1,2}:\d{2}/;
const DATED_ENTRY_TITLE = /^(?:(?:生时校正|今日节奏)\s*·\s*\d{1,2}月\d{1,2}日|\d{1,2}月\d{1,2}日\s*·\s*(?:今日节奏|生时校正))(?:\s+\d{2}:\d{2})?$/;
// Same automatic shapes as public.rectification_session_title_is_automatic.
// The range dash is U+2013. A hyphen range is not automatic. The parity test
// reads the SQL predicate from the migration and compares answers (BUG-1001).
const DATED_ENTRY_TITLE = /^(?:(?:生时校正|今日节奏)\s*·\s*[0-9]{1,2}月[0-9]{1,2}日|[0-9]{1,2}月[0-9]{1,2}日\s*·\s*(?:今日节奏|生时校正))(?:\s+[0-9]{2}:[0-9]{2})?$|^生时校正$|^生时校正\s*·\s*[0-9]{2}:[0-9]{2}$|^生时校正\s*·\s*[0-9]{2}:[0-9]{2}\u2013[0-9]{2}:[0-9]{2}$/;
export function sanitizeSessionTitle(raw: string): string | null {
if (/[\r\n]/.test(raw)) return null;
@@ -0,0 +1,116 @@
-- BUG-1001: rectification titles carry the result, not a date.
-- One formula and one automatic-title predicate. The trigger and the backfill
-- both call them. Do not assign chat_sessions.updated_at (BUG-988 / BUG-704).
-- Open / GET / metadata PATCH still must not retitle (BUG-699): this trigger
-- listens only to accepted_time, confirmed_time, and candidate_range.
begin;
do $migration$
begin
if current_user <> 'schema_owner' then
raise exception 'rectification_session_title_result_requires_schema_owner'
using errcode = '42501';
end if;
end
$migration$;
create or replace function public.rectification_session_title(
p_accepted time,
p_confirmed time,
p_range jsonb
) returns text
language sql
immutable
parallel safe
set search_path = ''
as $$
select case
when p_confirmed is not null then
'生时校正 · ' || to_char(p_confirmed, 'HH24:MI')
when p_accepted is not null then
'生时校正 · ' || to_char(p_accepted, 'HH24:MI')
when public.agentic_rectification_is_clock(p_range ->> 'start_time')
and public.agentic_rectification_is_clock(p_range ->> 'end_time') then
'生时校正 · ' || (p_range ->> 'start_time') || '–' || (p_range ->> 'end_time')
else
'生时校正'
end;
$$;
-- The dash in the range alternative is U+2013 EN DASH, not a hyphen.
-- frontend/src/lib/session-title.ts DATED_ENTRY_TITLE must agree with this
-- predicate. The parity test reads this pattern from this file.
create or replace function public.rectification_session_title_is_automatic(
p_title text
) returns boolean
language sql
immutable
parallel safe
set search_path = ''
as $$
select p_title is not null
and p_title ~ '^(?:(?:生时校正|今日节奏)\s*·\s*[0-9]{1,2}月[0-9]{1,2}日|[0-9]{1,2}月[0-9]{1,2}日\s*·\s*(?:今日节奏|生时校正))(?:\s+[0-9]{2}:[0-9]{2})?$|^生时校正$|^生时校正\s*·\s*[0-9]{2}:[0-9]{2}$|^生时校正\s*·\s*[0-9]{2}:[0-9]{2}–[0-9]{2}:[0-9]{2}$';
$$;
create or replace function public.sync_rectification_session_title_from_case()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
update public.chat_sessions
set title = public.rectification_session_title(new.accepted_time, new.confirmed_time, new.candidate_range)
where id = new.session_id
and user_id = new.user_id
and session_type = 'birth_time_rectification'
and title is distinct from public.rectification_session_title(new.accepted_time, new.confirmed_time, new.candidate_range)
and public.rectification_session_title_is_automatic(title);
return new;
end;
$$;
revoke all on function public.rectification_session_title(time, time, jsonb)
from public, anon, authenticated;
revoke all on function public.rectification_session_title_is_automatic(text)
from public, anon, authenticated;
revoke all on function public.sync_rectification_session_title_from_case()
from public, anon, authenticated;
grant execute on function public.rectification_session_title(time, time, jsonb)
to schema_owner;
grant execute on function public.rectification_session_title_is_automatic(text)
to schema_owner;
grant execute on function public.sync_rectification_session_title_from_case()
to schema_owner;
drop trigger if exists agentic_rectification_cases_title_from_result
on public.agentic_rectification_cases;
create trigger agentic_rectification_cases_title_from_result
after insert or update of accepted_time, confirmed_time, candidate_range
on public.agentic_rectification_cases
for each row
execute function public.sync_rectification_session_title_from_case();
do $backfill$
declare
retitled integer;
begin
-- BEGIN rectification_session_title_backfill
update public.chat_sessions as session
set title = public.rectification_session_title(case_row.accepted_time, case_row.confirmed_time, case_row.candidate_range)
from public.agentic_rectification_cases as case_row
where case_row.session_id = session.id
and case_row.user_id = session.user_id
and session.session_type = 'birth_time_rectification'
and session.title is distinct from public.rectification_session_title(case_row.accepted_time, case_row.confirmed_time, case_row.candidate_range)
and public.rectification_session_title_is_automatic(session.title);
-- END rectification_session_title_backfill
get diagnostics retitled = row_count;
raise notice 'rectification_session_title_result retitled=%', retitled;
end
$backfill$;
commit;
+5 -4
View File
@@ -59,9 +59,9 @@ test("general no-birth-time replies keep a question-specific session title", ()
});
test("daily and topic consultations get distinct dated or domain titles", () => {
// 原值:8月22日 · 今日节奏;同名追加墙钟 08:36;8月22日 · 生时校正
// 新值:今日节奏 · 8月22日;同名不再加 HH:MM;生时校正 · 8月22日
// 原因:任务书 T4 类别在前,同名靠副标题区分
// 原值:今日节奏 · 8月22日;同名不再加 HH:MM;生时校正 · 8月22日
// 新值:今日节奏 · 8月22日;同名不再加 HH:MM;生时校正
// 原因:BUG-1001 新建校正不带日期,结果时间由数据库触发器写入;今日节奏仍带日期(D3)
const at = new Date(Date.UTC(2026, 7, 22, 0, 36));
assert.equal(
resolveSessionTitle("深入看今日", undefined, { entrypoint: "daily_starlanguage", at }),
@@ -84,8 +84,9 @@ test("daily and topic consultations get distinct dated or domain titles", () =>
entrypoint: "birth_time_rectification",
at,
}),
"生时校正 · 8月22日",
"生时校正",
);
assert.equal(resolveSessionTitle("生时校正", undefined, { at }), "生时校正");
});
@@ -0,0 +1,323 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { spawnSync } from "node:child_process";
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { startPostgresFixture, type PostgresFixture } from "./helpers/postgres-fixture.ts";
const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
const migrationPath = new URL("../supabase/migrations/20260925010000_rectification_session_title_result.sql", import.meta.url);
const migrationSql = readFileSync(migrationPath, "utf8");
const docker = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { stdio: "ignore" }).status === 0;
const EN_DASH = "\u2013";
const schemaPassword = "schema-owner-test-password";
function windowsMigrationDirectory(): string {
// Git symlinks in frontend/db/migrations are plain relative-path files on
// Windows, so the dual-directory scan reports a duplicate filename. Linux
// skips those symlinks (Dirent.isFile() is false). This copy keeps one real
// SQL file per name and still runs scripts/db-migrate.mjs.
const directory = mkdtempSync(join(tmpdir(), "jyotisha-title-migrations-"));
const seen = new Map<string, Buffer>();
for (const relative of ["../db/migrations", "../supabase/migrations"]) {
const source = fileURLToPath(new URL(relative, import.meta.url));
for (const name of readdirSync(source)) {
if (!/^\d{14}_[a-z0-9_]+\.sql$/.test(name)) continue;
const bytes = readFileSync(join(source, name));
if (/^\.\.\/.+\.sql$/.test(bytes.toString("utf8").trim())) continue;
const previous = seen.get(name);
if (previous) {
assert.equal(bytes.equals(previous), true, `${name} mirrors must match`);
continue;
}
seen.set(name, bytes);
writeFileSync(join(directory, name), bytes);
}
}
assert.ok(seen.has("20260925010000_rectification_session_title_result.sql"));
return directory;
}
function applyMigrations(fixture: PostgresFixture): void {
const env = {
...process.env,
SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", schemaPassword),
};
const first = spawnSync(process.execPath, [runner], { encoding: "utf8", env });
if (first.status === 0) return;
const detail = `${first.stderr ?? ""}\n${first.stdout ?? ""}`;
assert.match(detail, /duplicate migration filename/, detail);
const directory = windowsMigrationDirectory();
try {
const second = spawnSync(process.execPath, [runner], {
encoding: "utf8",
env: { ...env, MIGRATIONS_DIRECTORY: directory },
});
assert.equal(second.status, 0, second.stderr || second.stdout);
} finally {
rmSync(directory, { force: true, recursive: true });
}
}
function backfillUpdate(): string {
const start = migrationSql.indexOf("-- BEGIN rectification_session_title_backfill");
const end = migrationSql.indexOf("-- END rectification_session_title_backfill");
assert.ok(start >= 0 && end > start, "backfill markers missing");
const block = migrationSql.slice(start, end);
const update = block.match(/update public\.chat_sessions as session[\s\S]*?;/)?.[0];
assert.ok(update, "backfill update must be cut from the migration, not copied");
assert.doesNotMatch(update, /updated_at\s*=/);
assert.match(update, /public\.rectification_session_title\(/);
assert.match(update, /public\.rectification_session_title_is_automatic\(/);
return update;
}
function utf8Hex(value: string): string {
return Buffer.from(value, "utf8").toString("hex");
}
function titleHex(fixture: PostgresFixture, sessionId: string): string {
return fixture.psql(
`select encode(convert_to(title, 'UTF8'), 'hex') from public.chat_sessions where id = '${sessionId}'`,
);
}
function identityFingerprint(fixture: PostgresFixture, sessionId: string): string {
return fixture.psql(
`select pinned::text || '|' || messages::text || '|' || updated_at::text from public.chat_sessions where id = '${sessionId}'`,
);
}
function countUpdate(fixture: PostgresFixture, sql: string): string {
const output = fixture.psqlScriptAs("schema_owner", schemaPassword, `
drop table if exists backfill_row_count;
create temp table backfill_row_count(n integer);
do $count$
declare n integer;
begin
${sql}
get diagnostics n = row_count;
insert into backfill_row_count values (n);
end
$count$;
select n from backfill_row_count;
`);
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
return lines[lines.length - 1] ?? "";
}
test("rectification session titles follow the result and do not bump updated_at", {
skip: docker ? false : "docker unavailable",
}, () => {
const fixture = startPostgresFixture();
try {
applyMigrations(fixture);
assert.doesNotMatch(migrationSql, /updated_at\s*=/);
const call = (sql: string) => fixture.psqlAs("schema_owner", schemaPassword, `select ${sql}`);
assert.equal(
call(`public.rectification_session_title('04:00'::time, '05:07'::time, '{"start_time":"05:00","end_time":"05:15"}'::jsonb)`),
"生时校正 · 05:07",
);
assert.equal(
call(`public.rectification_session_title('06:30'::time, null, '{"start_time":"05:00","end_time":"05:15"}'::jsonb)`),
"生时校正 · 06:30",
);
assert.equal(
call(`public.rectification_session_title(null, null, '{"start_time":"05:00","end_time":"05:15"}'::jsonb)`),
`生时校正 · 05:00${EN_DASH}05:15`,
);
assert.equal(
call(`public.rectification_session_title(null, null, '{"start_time":"25:00","end_time":"05:15"}'::jsonb)`),
"生时校正",
);
assert.equal(
call(`public.rectification_session_title(null, null, '{}'::jsonb)`),
"生时校正",
);
const volatility = fixture.psql(`
select proname || '=' || provolatile::text
from pg_proc
where pronamespace = 'public'::regnamespace
and proname in ('rectification_session_title', 'rectification_session_title_is_automatic')
order by proname
`);
assert.equal(
volatility,
"rectification_session_title=i\nrectification_session_title_is_automatic=i",
);
const bodies = fixture.psql(`
select string_agg(prosrc, ' ')
from pg_proc
where pronamespace = 'public'::regnamespace
and proname in ('rectification_session_title', 'rectification_session_title_is_automatic')
`);
assert.doesNotMatch(bodies, /\bfrom\b/i);
assert.equal(
call(`public.rectification_session_title_is_automatic('生时校正 · 05:00-05:15')`),
"f",
);
assert.equal(call(`public.rectification_session_title_is_automatic('我的校正')`), "f");
const triggerDef = fixture.psql(`
select pg_get_triggerdef(oid)
from pg_trigger
where tgname = 'agentic_rectification_cases_title_from_result'
and not tgisinternal
`);
assert.match(triggerDef, /AFTER INSERT OR UPDATE OF accepted_time, confirmed_time, candidate_range/i);
assert.doesNotMatch(triggerDef, /last_activity_at/);
assert.doesNotMatch(triggerDef, /updated_at/);
const touchDef = fixture.psql(`
select pg_get_triggerdef(oid)
from pg_trigger
where tgname = 'agentic_rectification_cases_touch_chat_session'
and not tgisinternal
`);
assert.match(touchDef, /UPDATE OF last_activity_at/i);
assert.doesNotMatch(touchDef, /accepted_time/);
const user = randomUUID();
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`insert into identity.users(id,name,email,email_verified) values('${user}','Fictional title','${user}@example.invalid',true)`,
);
const insertSession = (
id: string,
title: string,
sessionType: string,
updatedAt: string,
pinned: boolean,
message: string,
) => {
fixture.psql(`
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, pinned, updated_at)
values (
'${id}', '${user}', '${title}', 'general', '${sessionType}',
'[{"role":"user","text":"${message}"}]'::jsonb, ${pinned}, '${updatedAt}'
)
`);
};
const insertCase = (
sessionId: string,
accepted: string | null,
confirmed: string | null,
start: string,
end: string,
) => {
const id = randomUUID();
const acceptedSql = accepted ? `'${accepted}'::time` : "null";
const confirmedSql = confirmed ? `'${confirmed}'::time` : "null";
fixture.psql(`
insert into public.agentic_rectification_cases (
id, user_id, session_id, status, skill_name, skill_version,
baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range,
accepted_time, confirmed_time
) values (
'${id}', '${user}', '${sessionId}', 'candidate_ready',
'jyotish-birth-time-rectification', '9.0.0', '${"a".repeat(64)}',
'{"birth_date":"2000-01-01"}'::jsonb,
'{"start_time":"${start}","end_time":"${end}"}'::jsonb,
${acceptedSql}, ${confirmedSql}
)
`);
return id;
};
const adoptSession = randomUUID();
insertSession(adoptSession, "生时校正", "birth_time_rectification", "2026-09-11 08:00:00+00", true, "adopt");
const beforeAdoptInsert = identityFingerprint(fixture, adoptSession);
const adoptCase = insertCase(adoptSession, null, null, "05:00", "05:15");
assert.equal(identityFingerprint(fixture, adoptSession), beforeAdoptInsert);
assert.equal(titleHex(fixture, adoptSession), utf8Hex(`生时校正 · 05:00${EN_DASH}05:15`));
const beforeAdopt = identityFingerprint(fixture, adoptSession);
fixture.psql(`update public.agentic_rectification_cases set accepted_time = '05:07'::time where id = '${adoptCase}'`);
assert.equal(titleHex(fixture, adoptSession), utf8Hex("生时校正 · 05:07"));
assert.equal(identityFingerprint(fixture, adoptSession), beforeAdopt);
const narrowSession = randomUUID();
insertSession(narrowSession, "生时校正", "birth_time_rectification", "2026-09-12 08:00:00+00", false, "narrow");
const narrowCase = insertCase(narrowSession, null, null, "05:00", "05:15");
const beforeNarrow = identityFingerprint(fixture, narrowSession);
fixture.psql(`
update public.agentic_rectification_cases
set candidate_range = '{"start_time":"05:02","end_time":"05:08"}'::jsonb
where id = '${narrowCase}'
`);
assert.equal(titleHex(fixture, narrowSession), utf8Hex(`生时校正 · 05:02${EN_DASH}05:08`));
assert.equal(identityFingerprint(fixture, narrowSession), beforeNarrow);
const renamedSession = randomUUID();
insertSession(renamedSession, "生时校正", "birth_time_rectification", "2026-09-13 08:00:00+00", true, "renamed-live");
const renamedCase = insertCase(renamedSession, null, null, "08:00", "08:10");
fixture.psql(`update public.chat_sessions set title = '我的校正' where id = '${renamedSession}'`);
const beforeRenameAdopt = identityFingerprint(fixture, renamedSession);
fixture.psql(`update public.agentic_rectification_cases set accepted_time = '08:08'::time where id = '${renamedCase}'`);
assert.equal(titleHex(fixture, renamedSession), utf8Hex("我的校正"));
assert.equal(identityFingerprint(fixture, renamedSession), beforeRenameAdopt);
const touchSession = randomUUID();
insertSession(touchSession, "生时校正", "birth_time_rectification", "2026-09-10 08:00:00+00", false, "touch");
const touchCase = insertCase(touchSession, null, null, "04:10", "04:20");
const titleBeforeTouch = titleHex(fixture, touchSession);
fixture.psql(`
update public.agentic_rectification_cases
set last_activity_at = '2026-09-20 12:00:00+00'
where id = '${touchCase}'
`);
assert.equal(titleHex(fixture, touchSession), titleBeforeTouch);
assert.equal(
fixture.psql(`
select (session.updated_at = case_row.last_activity_at)::text
from public.chat_sessions as session
join public.agentic_rectification_cases as case_row on case_row.session_id = session.id
where session.id = '${touchSession}'
`),
"true",
);
fixture.psql("alter table public.agentic_rectification_cases disable trigger agentic_rectification_cases_title_from_result");
const rows = [
{ key: "confirmed", title: "生时校正", accepted: "04:00", confirmed: "05:07", start: "05:00", end: "05:15", expected: "生时校正 · 05:07", pinned: true, at: "2026-09-01 01:00:00+00" },
{ key: "accepted", title: "生时校正", accepted: "06:30", confirmed: null, start: "05:00", end: "05:15", expected: "生时校正 · 06:30", pinned: false, at: "2026-09-02 01:00:00+00" },
{ key: "range", title: "生时校正", accepted: null, confirmed: null, start: "05:00", end: "05:15", expected: `生时校正 · 05:00${EN_DASH}05:15`, pinned: true, at: "2026-09-03 01:00:00+00" },
{ key: "old-clock", title: "9月17日 · 生时校正 09:35", accepted: null, confirmed: null, start: "04:10", end: "04:20", expected: `生时校正 · 04:10${EN_DASH}04:20`, pinned: false, at: "2026-09-04 01:00:00+00" },
{ key: "old-plain", title: "9月17日 · 生时校正", accepted: "07:07", confirmed: null, start: "01:00", end: "02:00", expected: "生时校正 · 07:07", pinned: true, at: "2026-09-05 01:00:00+00" },
{ key: "renamed", title: "我的校正", accepted: "08:08", confirmed: "08:09", start: "08:00", end: "08:10", expected: "我的校正", pinned: false, at: "2026-09-06 01:00:00+00" },
] as const;
const ids = new Map<string, string>();
for (const row of rows) {
const id = randomUUID();
ids.set(row.key, id);
insertSession(id, row.title, "birth_time_rectification", row.at, row.pinned, row.key);
insertCase(id, row.accepted, row.confirmed, row.start, row.end);
}
const orphan = randomUUID();
insertSession(orphan, "生时校正", "birth_time_rectification", "2026-09-07 01:00:00+00", true, "orphan");
const consultation = randomUUID();
insertSession(consultation, "生时校正", "consultation", "2026-09-08 01:00:00+00", false, "consultation");
const watched = [...ids.values(), orphan, consultation];
const before = new Map(watched.map((id) => [id, identityFingerprint(fixture, id)]));
const update = backfillUpdate();
assert.equal(countUpdate(fixture, update), "5");
for (const row of rows) {
const id = ids.get(row.key);
assert.ok(id);
assert.equal(titleHex(fixture, id), utf8Hex(row.expected), row.key);
assert.equal(identityFingerprint(fixture, id), before.get(id), row.key);
}
assert.equal(titleHex(fixture, orphan), utf8Hex("生时校正"));
assert.equal(titleHex(fixture, consultation), utf8Hex("生时校正"));
assert.equal(identityFingerprint(fixture, orphan), before.get(orphan));
assert.equal(identityFingerprint(fixture, consultation), before.get(consultation));
assert.equal(countUpdate(fixture, update), "0");
fixture.psql("alter table public.agentic_rectification_cases enable trigger agentic_rectification_cases_title_from_result");
} finally {
fixture.stop();
}
});
@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { isAutoDerivedSessionTitle, shouldGenerateSessionTitle } from "../src/lib/session-title.ts";
// BUG-987 and BUG-992 were two copies of one rule with nobody comparing them.
// This test reads the SQL predicate from the migration. Do not paste a second copy.
const migration = readFileSync(
new URL("../supabase/migrations/20260925010000_rectification_session_title_result.sql", import.meta.url),
"utf8",
);
const functionBody = migration.match(
/create or replace function public\.rectification_session_title_is_automatic[\s\S]*?as \$\$([\s\S]*?)\$\$;/,
);
assert.ok(functionBody, "migration must define rectification_session_title_is_automatic");
const pattern = functionBody[1].match(/~ '([^']+)'/)?.[1];
assert.ok(pattern, "automatic-title predicate must be one quoted regex");
const sqlWouldOverwrite = new RegExp(pattern);
const EN_DASH = "\u2013";
const cases: ReadonlyArray<readonly [string, boolean]> = [
["生时校正", true],
["生时校正 · 05:07", true],
[`生时校正 · 05:00${EN_DASH}05:15`, true],
["生时校正 · 9月17日", true],
["9月17日 · 生时校正", true],
["9月17日 · 生时校正 09:35", true],
["我的校正", false],
["今日节奏 · 9月17日", true],
["9月17日 · 今日节奏", true],
["9月17日 · 今日节奏 09:35", true],
];
test("SQL overwrite predicate and isAutoDerivedSessionTitle agree", () => {
assert.match(pattern, new RegExp(String.raw`\[0-9\]\{2\}:\[0-9\]\{2\}${EN_DASH}\[0-9\]\{2\}:\[0-9\]\{2\}`));
assert.doesNotMatch(pattern, /\[0-9\]\{2\}:\[0-9\]\{2\}-\[0-9\]\{2\}:\[0-9\]\{2\}/);
for (const [title, automatic] of cases) {
assert.equal(sqlWouldOverwrite.test(title), automatic, `sql ${title}`);
assert.equal(isAutoDerivedSessionTitle(title), automatic, `ts ${title}`);
}
assert.equal(sqlWouldOverwrite.test(`生时校正 · 05:00-05:15`), false);
assert.equal(shouldGenerateSessionTitle({ title: "生时校正", sessionType: "birth_time_rectification" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "生时校正 · 05:07", sessionType: "birth_time_rectification" }, []), false);
});
@@ -1,60 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
datedRectificationTitle,
repairedRectificationTitle,
} from "../src/lib/rectification-session-title-repair.ts";
test("dated titles use the product calendar, not UTC", () => {
const createdAt = new Date("2026-09-11T18:30:00.000Z");
assert.equal(datedRectificationTitle(createdAt), "9月12日 · 生时校正");
});
test("repair rewrites a wall-clock today stamp back to created_at's local date", () => {
assert.equal(
repairedRectificationTitle({
title: "9月15日 · 生时校正",
createdAt: new Date("2026-09-12T03:00:00.000Z"),
}),
"9月12日 · 生时校正",
);
assert.equal(
repairedRectificationTitle({
title: "9月15日 · 生时校正 14:32",
createdAt: new Date("2026-09-12T03:00:00.000Z"),
}),
"9月12日 · 生时校正",
);
});
test("repair leaves matching dates and user-renamed titles alone", () => {
assert.equal(
repairedRectificationTitle({
title: "9月12日 · 生时校正",
createdAt: new Date("2026-09-12T03:00:00.000Z"),
}),
null,
);
assert.equal(
repairedRectificationTitle({
title: "9月12日 · 生时校正 14:32",
createdAt: new Date("2026-09-12T03:00:00.000Z"),
}),
null,
);
assert.equal(
repairedRectificationTitle({
title: "家里那次校正",
createdAt: new Date("2026-09-12T03:00:00.000Z"),
}),
null,
);
});
test("missing created_at falls back to a dateless title instead of guessing", () => {
assert.equal(
repairedRectificationTitle({ title: "9月15日 · 生时校正", createdAt: null }),
"生时校正",
);
});
+4 -1
View File
@@ -47,7 +47,10 @@ test("empty consultations stay off the sidebar while rectification rows stay on
const rectification = session({
id: "rect",
sessionType: "birth_time_rectification",
title: "生时校正 · 9月14日",
// 原值:生时校正 · 9月14日
// 新值:生时校正 · 05:07
// 原因:BUG-1001 校正标题改为结果时间;本断言只锁是否入列,不锁标题格式
title: "生时校正 · 05:07",
});
const archivedRectification = session({
id: "archived-rect",
@@ -35,8 +35,12 @@ test("shouldGenerateSessionTitle only runs on a first-round auto title", () => {
assert.equal(shouldGenerateSessionTitle({ title: "我想问一下最近半年换工…", sessionType: "consultation" }, []), true);
assert.equal(shouldGenerateSessionTitle({ title: "新对话", sessionType: "consultation" }, [{ role: "user" }]), false);
assert.equal(shouldGenerateSessionTitle({ title: "新对话", sessionType: "birth_time_rectification" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "生时校正", sessionType: "birth_time_rectification" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "生时校正 · 05:07", sessionType: "birth_time_rectification" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "9月6日 · 今日节奏", sessionType: "consultation" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "今日节奏 · 9月6日", sessionType: "consultation" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "生时校正", sessionType: "consultation" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "生时校正 · 05:07", sessionType: "consultation" }, []), false);
assert.equal(shouldGenerateSessionTitle({ title: "半年内换工作时机", sessionType: "consultation" }, []), false);
});