From 21a121a6a7b84e71beca0608e9c7151670762775 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 3 Aug 2026 16:07:18 +0800 Subject: [PATCH] fix(rectification): accept persisted birth dates --- docs/BUG_HISTORY.md | 16 ++++++++++++++++ frontend/src/app/page.tsx | 2 ++ .../src/lib/rectification-agentic/session.ts | 7 +++++-- .../tests/rectification-agentic-entry.test.ts | 17 +++++++++++++++++ .../tests/rectification-agentic-session.test.ts | 14 ++++++++++++++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 28206467..4107745a 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2007,3 +2007,19 @@ - 相关记录:BUG-112、BUG-113 - 复发自:BUG-113 - 修复版本:待本次 staging 修复提交与部署验收 + +## BUG-115 | ISO 出生日期被 Agentic 资料门误判并触发 opening 重试循环 + +- 状态: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`。 +- 触发条件:数据库驱动把出生日期投影为 `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 序列化形态;任何自动挂载请求的失败回调都必须先阻断对应的自动恢复条件。 +- 相关记录:BUG-016、BUG-114 +- 复发自:BUG-114 +- 修复版本:待本次 staging 修复提交与部署验收 diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index a0caf63c..d178cdd2 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1816,6 +1816,7 @@ export default function Home() { await persistProfile(profileDraft); setProfile(profileDraft); setProfileDraft(profileDraft); + setRectificationError(""); if (declarationChanged) { setBirthTimeConsultationConsent(createBirthTimeConsultationConsentState()); setAccount((current) => current ? { ...current, rectificationCase: null } : current); @@ -2073,6 +2074,7 @@ export default function Home() { }; function handleRectificationProfileIncomplete() { + setRectificationError("profile_incomplete"); setRectificationSessionId(null); setRectificationPendingQuestion(null); const missingStep = missingProfileStep(profile); diff --git a/frontend/src/lib/rectification-agentic/session.ts b/frontend/src/lib/rectification-agentic/session.ts index 470a3de9..072f22b2 100644 --- a/frontend/src/lib/rectification-agentic/session.ts +++ b/frontend/src/lib/rectification-agentic/session.ts @@ -1,5 +1,6 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { AgenticRectificationContext } from "@/mastra/rectification-tools"; +import { parseBirthDate } from "../birth-time-intake-model.ts"; /** * Agentic rectification session support. @@ -133,8 +134,10 @@ export async function loadAgenticRectificationProfile( .single(); if (error || !data) throw new AgenticRectificationProfileError("profile_unavailable"); - const birthDate = typeof data.birth_date === "string" ? data.birth_date.trim() : ""; - if (!/^\d{4}-\d{2}-\d{2}$/.test(birthDate)) { + 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 activeTime = timeValue(data.active_birth_time); diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index 9323e629..c53fb0a1 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -41,3 +41,20 @@ test("incomplete profiles stay in the shared onboarding flow", () => { ); assert.match(chat, /payload\?\.code === "profile_incomplete"/); }); + +test("server profile failures pause automatic rectification resume", () => { + const resumeEffect = page.slice( + page.indexOf('useEffect(() => {\n if (!hydrated'), + page.indexOf('useEffect(() => {\n if (!hydrated || !accountId'), + ); + const incompleteHandler = page.slice( + page.indexOf("function handleRectificationProfileIncomplete"), + page.indexOf("async function draftSynastryQuestionFromChart"), + ); + assert.match(resumeEffect, /\|\| rectificationError\) return/); + assert.match(incompleteHandler, /setRectificationError\("profile_incomplete"\)/); + assert.ok( + incompleteHandler.indexOf('setRectificationError("profile_incomplete")') + < incompleteHandler.indexOf("setRectificationSessionId(null)"), + ); +}); diff --git a/frontend/tests/rectification-agentic-session.test.ts b/frontend/tests/rectification-agentic-session.test.ts index e0328d2d..780d0ecd 100644 --- a/frontend/tests/rectification-agentic-session.test.ts +++ b/frontend/tests/rectification-agentic-session.test.ts @@ -61,6 +61,20 @@ test("loadAgenticRectificationProfile derives birth fields, accuracy and baselin assert.equal(profile.baselineActiveTime, "14:31"); }); +test("loadAgenticRectificationProfile normalizes a persisted ISO birth date", async () => { + const { client } = fakeAccounting(fakeProfileRow({ birth_date: "1997-08-08T00:00:00.000Z" })); + const profile = await loadAgenticRectificationProfile(client as never, userId); + assert.equal(profile.birth_date, "1997-08-08"); +}); + +test("loadAgenticRectificationProfile rejects an invalid persisted birth date", async () => { + const { client } = fakeAccounting(fakeProfileRow({ birth_date: "1997-02-30T00:00:00.000Z" })); + await assert.rejects( + () => loadAgenticRectificationProfile(client as never, userId), + (error) => error instanceof AgenticRectificationProfileError && error.code === "missing_birth_date", + ); +}); + test("loadAgenticRectificationProfile treats hospital source as minute accuracy", async () => { const { client } = fakeAccounting(fakeProfileRow({ birth_time_source: "hospital",