diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 4107745a..a7462b3a 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2013,13 +2013,13 @@ - 状态:resolved(local) - 首次发现:2026-08-03 - 最近更新:2026-08-03 -- 影响面:`POST /api/rectification/agent`、首页 Session 自动恢复、`GET /api/account` 请求频率 -- 用户现象:账户接口已返回完整出生日期、时间线索和地点,Agent opening 仍返回 `profile_incomplete`;页面随后重复请求 `/api/account` 和 `/api/rectification/agent`。 +- 影响面:`POST /api/rectification/agent`、首页账户资料重新加载、Session 自动恢复、`GET /api/account` 请求频率 +- 用户现象:账户接口已返回完整出生日期、时间线索和地点,Agent opening 仍返回 `profile_incomplete`;页面随后重复请求 `/api/account` 和 `/api/rectification/agent`。部署首轮修复后,刷新页面还会错误回到“先完成出生资料”。 - 触发条件:数据库驱动把出生日期投影为 `YYYY-MM-DDT00:00:00.000Z`,同时当前活动 Session 是 `birth_time_rectification`。 -- 根因:Agentic profile loader 只接受纯 `YYYY-MM-DD`;失败回调清空 `rectificationSessionId` 却未设置现有的自动恢复暂停状态,resume effect 立即重新挂载聊天并再次发送 opening。 -- 修复:在共享 profile loader 边界提取并复用现有日历日期校验;服务端资料失败时先设置 `rectificationError` 暂停自动恢复,资料成功保存后再清除暂停状态。 -- 验证:回归覆盖 ISO 日期规范化、非法日历日期拒绝,以及 profile failure 在清空 Session 前设置自动恢复暂停状态。 -- 防复发:数据库日期边界不得假设唯一 JavaScript 序列化形态;任何自动挂载请求的失败回调都必须先阻断对应的自动恢复条件。 +- 根因:Agentic profile loader 和首页 `readProfile()` 都把持久化日期直接交给只接受纯 `YYYY-MM-DD` 的资料完整性校验;失败回调清空 `rectificationSessionId` 却未设置现有的自动恢复暂停状态,resume effect 立即重新挂载聊天并再次发送 opening。 +- 修复:增加共享持久化日期规范化函数,由 Agent loader 与首页账户重新加载共同复用;服务端资料失败时先设置 `rectificationError` 暂停自动恢复,资料成功保存后再清除暂停状态。 +- 验证:回归覆盖数据库 ISO 日期在 Agent loader 与首页重新加载中的规范化、非法日历日期拒绝,以及 profile failure 在清空 Session 前设置自动恢复暂停状态。 +- 防复发:数据库日期边界不得假设唯一 JavaScript 序列化形态;所有从账户持久化资料进入完整性校验的路径必须先走同一规范化函数;任何自动挂载请求的失败回调都必须先阻断对应的自动恢复条件。 - 相关记录:BUG-016、BUG-114 - 复发自:BUG-114 - 修复版本:待本次 staging 修复提交与部署验收 diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d178cdd2..f2a4d1b4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -37,6 +37,7 @@ import { describeBirthTimeDraft, isDeclaredBirthProfileComplete, isBirthTimeDraftReady, + normalizePersistedBirthDate, type BirthTimeDraft, type BirthTimeSource, } from "@/lib/birth-time-intake-model"; @@ -607,7 +608,9 @@ function readProfile(value: unknown): Profile { longitude?: unknown; timezone_offset?: unknown; }; - const date = typeof profile.birth_date === "string" ? profile.birth_date : profile.date; + const date = normalizePersistedBirthDate( + typeof profile.birth_date === "string" ? profile.birth_date : profile.date, + ); const legacyTime = typeof profile.birth_time === "string" ? profile.birth_time.slice(0, 5) : profile.time; const time = typeof profile.active_birth_time === "string" ? profile.active_birth_time.slice(0, 5) diff --git a/frontend/src/lib/birth-time-intake-model.ts b/frontend/src/lib/birth-time-intake-model.ts index 1badf878..b81572a4 100644 --- a/frontend/src/lib/birth-time-intake-model.ts +++ b/frontend/src/lib/birth-time-intake-model.ts @@ -63,6 +63,13 @@ export function parseBirthDate(value: string): Date | undefined { return parsed; } +export function normalizePersistedBirthDate(value: unknown): string { + if (typeof value !== "string") return ""; + const persisted = value.trim(); + const date = persisted.slice(0, 10); + return parseBirthDate(date) && (persisted.length === 10 || persisted[10] === "T") ? date : ""; +} + export function isBirthClockTime(value: string): boolean { return birthClockPattern.test(value); } diff --git a/frontend/src/lib/rectification-agentic/session.ts b/frontend/src/lib/rectification-agentic/session.ts index 072f22b2..18d3c684 100644 --- a/frontend/src/lib/rectification-agentic/session.ts +++ b/frontend/src/lib/rectification-agentic/session.ts @@ -1,6 +1,6 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { AgenticRectificationContext } from "@/mastra/rectification-tools"; -import { parseBirthDate } from "../birth-time-intake-model.ts"; +import { normalizePersistedBirthDate } from "../birth-time-intake-model.ts"; /** * Agentic rectification session support. @@ -134,12 +134,8 @@ export async function loadAgenticRectificationProfile( .single(); if (error || !data) throw new AgenticRectificationProfileError("profile_unavailable"); - const persistedBirthDate = typeof data.birth_date === "string" ? data.birth_date.trim() : ""; - const birthDate = persistedBirthDate.slice(0, 10); - if (!parseBirthDate(birthDate) - || (persistedBirthDate.length > 10 && persistedBirthDate[10] !== "T")) { - throw new AgenticRectificationProfileError("missing_birth_date"); - } + const birthDate = normalizePersistedBirthDate(data.birth_date); + if (!birthDate) throw new AgenticRectificationProfileError("missing_birth_date"); const activeTime = timeValue(data.active_birth_time); const reportedTime = timeValue(data.reported_birth_time); const lat = numberOrNull(data.latitude); diff --git a/frontend/tests/birth-time-intake.test.ts b/frontend/tests/birth-time-intake.test.ts index 948df6f7..69f81366 100644 --- a/frontend/tests/birth-time-intake.test.ts +++ b/frontend/tests/birth-time-intake.test.ts @@ -11,6 +11,7 @@ import { isDeclaredBirthProfileComplete, isBirthTimeReadyForConsultation, isBirthTimeDraftReady, + normalizePersistedBirthDate, parseBirthDate, type BirthTimeDraft, } from "../src/lib/birth-time-intake-model.ts"; @@ -267,6 +268,13 @@ test("birth date values round trip leap days and reject invalid input", () => { assert.equal(parseBirthDate("2001-02-29"), undefined); }); +test("persisted birth dates normalize database ISO values without accepting invalid dates", () => { + assert.equal(normalizePersistedBirthDate("1997-08-08T00:00:00.000Z"), "1997-08-08"); + assert.equal(normalizePersistedBirthDate("1997-08-08"), "1997-08-08"); + assert.equal(normalizePersistedBirthDate("1997-02-30T00:00:00.000Z"), ""); + assert.equal(normalizePersistedBirthDate("1997-08-08junk"), ""); +}); + test("candidate copy does not claim an unconfirmed minute is automatically in use", () => { const source = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8"); diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index c53fb0a1..ee94769c 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -58,3 +58,11 @@ test("server profile failures pause automatic rectification resume", () => { < incompleteHandler.indexOf("setRectificationSessionId(null)"), ); }); + +test("account rehydration normalizes persisted ISO birth dates before completeness checks", () => { + const profileReader = page.slice( + page.indexOf("function readProfile"), + page.indexOf("function readSessions"), + ); + assert.match(profileReader, /const date = normalizePersistedBirthDate\(/); +});