diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index ca940f48..d28cd6a4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -19947,3 +19947,19 @@ - 相关记录:issue #114。 - 复发自:无 - 修复版本:未修。 + +## BUG-1464 | 存过别人的账户进首页即「页面出错了」 + +- 状态:resolved +- 首次发现:2026-10-27(issue #115,测试环境首页) +- 最近更新:2026-10-27 +- 影响面:首页。正式站当时还没有这段启程代码,不受影响。 +- 用户现象:打开首页显示「页面出错了」。控制台是 `TypeError: Cannot read properties of undefined (reading 'length')`,以及 `app root error`。已保存的对话不受影响。 +- 触发条件:账户里存过至少一个有名字、有出生日期的别人。本机若还留着「先看某某的盘」的引导记录,会按 id 短路,暂时不触发。 +- 根因:`GET /api/chart-profiles` 的 `libraryProfileFromSubjectRow` 不返回 `birthTimeClue` 和 `rectificationCaseId`(库里没有这两列)。`normalizeChartLibraryApiRecord` 把 profile 原样摊开。2026-10-24 `4d63475caf` 起,首页每次渲染都对人物列表里有名字的别人调用 `missingOtherProfileStep`,日期能解析时读 `birthTimeClue.length`,字段不在就抛错,整页崩溃。 +- 修复:在 `normalizeChartLibraryApiRecord` 先铺 `emptyProfile`,再铺接口字段,最后写 `chartRelationship`。接口给了的字段仍以接口为准。不改库、不改接口。 +- 验证:`frontend/tests/chart-library-normalize-20261027.test.ts`。虚构的精确时间、大概时段、只有名字三种,经入口后 `emptyProfile` 的键都在;`missingOtherProfileStep` 分别为完成、`place`、`birth`,不再抛错。缺字段的接口形状在补齐前仍会抛 `length`。 +- 防复发:上面的测试锁住「人物列表 profile 键齐全」,以及首页用的同一条人物查找不再因缺字段崩溃。 +- 相关记录:`4d63475caf`。issue #115。 +- 复发自:无 +- 修复版本:分支 `codex/home-crash-saved-people-20261027`。 diff --git a/docs/tasks/PROGRESS-home-crash-saved-people-20261027.md b/docs/tasks/PROGRESS-home-crash-saved-people-20261027.md new file mode 100644 index 00000000..a7cf12eb --- /dev/null +++ b/docs/tasks/PROGRESS-home-crash-saved-people-20261027.md @@ -0,0 +1,34 @@ +# 进度:存过别人的账户首页崩溃(issue #115,2026-10-27) + +- 任务书:`docs/tasks/TASK-home-crash-saved-people-20261027.md` +- 分支:`codex/home-crash-saved-people-20261027`(从 `origin/staging` `e34dc6b66` 新建;任务书基线 `941effe80`,其上只有这份任务书) +- 执行:Grok。BUG-1464。开工时最大号 BUG-1463,本单用 1464。 + +## 做了什么 + +人物列表从接口拿回来时,先铺一份空的个人资料,再盖上接口给的字段。缺的 `birthTimeClue` 和 `rectificationCaseId` 变成空字符串,首页不再因为读不到长度而整页报错。接口已经给了的名字、日期、时段、关系保持原样。没有改数据库,也没有改接口。 + +首页用的那条「找一个还没填完的别人」没有单独渲染整页 hook。崩溃就发生在这条查找上,测试直接跑同一条判断。渲染 hook 要整页账户状态,这一层已经能复现报错,也能证明补齐后不再报。 + +## 改动的既有断言 + +没有改既有断言。 + +## 验收数字 + +| 检查 | 命令 | 结果 | +| --- | --- | --- | +| 类型 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 | +| 规范 | `frontend` 下 `npm run lint` | 0 error,122 warning(原有,未改) | +| 新测试 | `node node_modules/tsx/dist/cli.mjs --test tests/chart-library-normalize-20261027.test.ts` | 2 通过,0 失败,0 cancelled | +| 相关测试 | 同上,外加 `chart-library-other-profile`、`daily-starlanguage`、`people-archive-regressions` | 38 通过,0 失败,0 cancelled | +| 全量 | `frontend` 下 `npm test` | 5268 项,5235 通过,33 失败,0 cancelled | + +33 条失败里,13 条与 `tools/agent-loop/windows-known-failures.txt` 的测试名逐条相同(数据库套件在 Windows 上的已知失败)。另外 20 条是 Windows 环境:路径变成 `G:\G:\`、shell / 部署脚本、docker 参数过长、临时目录被占用、三个「新建对话」预取超时。没有一条落在这次改的人物资料入口上。失败总数仍是先前 Windows 干净跑的 33。 + +`npm run test:db` 本单不要求,未跑。没有登录态,没有在浏览器里打开首页。真机步骤写在 `docs/testing/ui-kit-migration-20261023.md` 的「首页进不去」一节。 + +## 偏离 + +- 分支从执行时的 `origin/staging` `e34dc6b66` 新建。任务书写的基线是 `941effe80`,两者之间只有这份任务书。 +- 没有渲染 `useProfileOnboarding`。首页崩溃发生在它每次渲染时的人物查找上,测试直接跑同一条判断。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 69fa004e..30de8d08 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -541,4 +541,4 @@ | `TASK-lagna-precheck-fix-20261027.md` | — | **#112 第 1 次打回**:已有进行中的校正时点链接报冲突(应走首页续上);别人的星盘页不该给校正链接 | 待执行 | `codex/lagna-precheck-20261027` | | `TASK-voice-reading-20261027.md` | `PROGRESS-voice-reading-20261027.md` | **#113 选 R1 C1**:报告前面加一段模型写的「解读」(八节,素材只用引擎事实,失败不影响报告);对话依据写进正文、宫位带生活领域、时间最多三件(BUG-1460、BUG-1461) | Claude 直接执行,待验收 | `codex/voice-reading-20261027` | | — | `PROGRESS-chart-ui-114-20261027.md` | **#114 星盘页一页铺开**:南北印度可点盘、检视、深读四页、对话右栏完整盘面(直接执行) | 待推 staging | `codex/chart-ui-114-20261027` | -| `TASK-home-crash-saved-people-20261027.md` | — | **#115 首页崩溃**:存过别人的账户一进首页就「页面出错了」——人物列表的资料缺两个字段,启程引导一读就报错;入口补齐默认值(BUG-1464) | 待领取 | `codex/home-crash-saved-people-20261027` | +| `TASK-home-crash-saved-people-20261027.md` | `PROGRESS-home-crash-saved-people-20261027.md` | **#115 首页崩溃**:存过别人的账户一进首页就「页面出错了」——人物列表的资料缺两个字段,启程引导一读就报错;入口补齐默认值(BUG-1464) | 待验收 | `codex/home-crash-saved-people-20261027` | diff --git a/docs/testing/ui-kit-migration-20261023.md b/docs/testing/ui-kit-migration-20261023.md index c5d437b6..44de5564 100644 --- a/docs/testing/ui-kit-migration-20261023.md +++ b/docs/testing/ui-kit-migration-20261023.md @@ -129,3 +129,9 @@ 5. 出生时间只知道大概时段的账户,首问还没用时:第一问是「月亮在……,我是个什么样的人」。上升不会换座的话,是「上升……、月亮……,我是个什么样的人」。 6. 给别人建完档,天空过场下面有「那一刻,{名字}的上升在……」。那个人的出生时间不确定、上升会换座时,没有这一句,过场照常。 +## 首页进不去(2026-10-27,issue #115) + +没有登录态,下面没在浏览器里做过。用存过别人的账户看。不要把出生资料、邮箱或会话原文写进记录。 + +1. 打开首页。页面正常,不再出现「页面出错了」。切换人物、打开星盘页,都照常。 + diff --git a/frontend/src/lib/home-cloud-sync.ts b/frontend/src/lib/home-cloud-sync.ts index ca40a805..66f1aaa5 100644 --- a/frontend/src/lib/home-cloud-sync.ts +++ b/frontend/src/lib/home-cloud-sync.ts @@ -18,6 +18,7 @@ import { import { normalizeConsultationDomain } from "@/lib/consultation-domain-registry"; import { notifyOnboardingFactsChanged } from "@/lib/onboarding-signals"; import { + emptyProfile, timestamp, undoWindowMs, uuidPattern, @@ -187,7 +188,7 @@ export function normalizeChartLibraryApiRecord(record: ChartLibraryApiRecord): C return { id: record.role === "self" ? "self" : record.id, role: record.role, - profile: { ...record.profile, chartRelationship: relationship }, + profile: { ...emptyProfile, ...record.profile, chartRelationship: relationship }, relationship, updatedAt: Date.parse(record.updated_at || "") || timestamp(), }; diff --git a/frontend/tests/chart-library-normalize-20261027.test.ts b/frontend/tests/chart-library-normalize-20261027.test.ts new file mode 100644 index 00000000..49393cc4 --- /dev/null +++ b/frontend/tests/chart-library-normalize-20261027.test.ts @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { libraryProfileFromSubjectRow } from "../src/lib/chart-subject-write.ts"; +import { normalizeChartLibraryApiRecord } from "../src/lib/home-cloud-sync.ts"; +import { missingOtherProfileStep } from "../src/lib/home-profile.ts"; +import { emptyProfile, type ChartLibraryApiRecord, type ChartLibraryRecord, type Profile } from "../src/lib/home-types.ts"; + +/** Fictional people. These dates and 杭州 are not anyone's birth data. */ +const exactRow = { + name: "林遥", + birth_date: "1990-01-02", + reported_birth_time: "08:30", + birth_time_source: "family_exact", + birth_time_status: "reported", + uncertainty_before_minutes: 0, + uncertainty_after_minutes: 0, + birth_place_label: "杭州", + latitude: 30.27, + longitude: 120.15, + timezone_id: "Asia/Shanghai", + timezone_offset: 8, + relation: "friend", + profile: {}, +}; + +const periodRow = { + name: "小舟", + birth_date: "1992-03-12", + birth_time_source: "period_only", + birth_time_period: "morning", + declared_window_start: "08:00", + declared_window_end: "11:59", + relation: "child", + profile: {}, +}; + +const nameOnlyRow = { + name: "阿宁", + relation: "parent", + profile: {}, +}; + +function apiRecord(id: string, row: Record, chartRelationship?: Profile["chartRelationship"]): ChartLibraryApiRecord { + const profile = libraryProfileFromSubjectRow(row) as ChartLibraryApiRecord["profile"]; + return { + id, + role: "other", + profile: chartRelationship ? { ...profile, chartRelationship } : profile, + updated_at: "2026-10-27T00:00:00.000Z", + }; +} + +function assertEmptyProfileKeys(profile: Profile) { + for (const key of Object.keys(emptyProfile) as (keyof typeof emptyProfile)[]) { + assert.equal(Object.prototype.hasOwnProperty.call(profile, key), true, String(key)); + const sample = emptyProfile[key]; + const actual = profile[key]; + if (sample === null) { + assert.ok(actual === null || typeof actual === "number", String(key)); + } else { + assert.equal(typeof actual, typeof sample, String(key)); + } + } +} + +/** Same predicate as pendingOther in use-profile-onboarding.ts when no intake hint is stored. */ +function pendingOther(rows: readonly ChartLibraryRecord[]) { + return rows.find((row) => { + if (row.role === "self") return false; + return Boolean(row.profile.name.trim()) && missingOtherProfileStep(row.profile) !== null; + }); +} + +test("a saved person from the chart API keeps every profile key and does not crash onboarding", () => { + const exact = normalizeChartLibraryApiRecord(apiRecord("other-exact", exactRow)); + const period = normalizeChartLibraryApiRecord(apiRecord("other-period", periodRow, "family")); + const nameOnly = normalizeChartLibraryApiRecord(apiRecord("other-name", nameOnlyRow)); + + for (const row of [exact, period, nameOnly]) assertEmptyProfileKeys(row.profile); + + assert.equal(missingOtherProfileStep(exact.profile), null); + assert.equal(missingOtherProfileStep(period.profile), "place"); + assert.equal(missingOtherProfileStep(nameOnly.profile), "birth"); + + assert.equal(period.profile.name, "小舟"); + assert.equal(period.profile.date, "1992-03-12"); + assert.equal(period.profile.birthTimePeriod, "morning"); + assert.equal(period.profile.declaredWindowStart, "08:00"); + assert.equal(period.profile.declaredWindowEnd, "11:59"); + assert.equal(period.profile.relation, "child"); + assert.equal(period.profile.chartRelationship, "family"); + assert.equal(period.relationship, "family"); + assert.equal(period.profile.birthTimeClue, ""); + assert.equal(period.profile.rectificationCaseId, ""); + + assert.equal(exact.profile.name, "林遥"); + assert.equal(exact.profile.date, "1990-01-02"); + assert.equal(exact.profile.reportedTime, "08:30"); + assert.equal(exact.profile.relation, "friend"); + assert.equal(exact.profile.birthPlaceLabel, "杭州"); + assert.equal(nameOnly.profile.name, "阿宁"); + assert.equal(nameOnly.profile.relation, "parent"); + assert.equal(nameOnly.profile.chartRelationship, "other"); + + const found = pendingOther([exact, period, nameOnly]); + assert.equal(found?.id, "other-period"); +}); + +test("the API shape that crashed the home page throws before normalize and not after", () => { + const raw = libraryProfileFromSubjectRow(exactRow) as Profile; + assert.equal("birthTimeClue" in raw, false); + assert.throws(() => missingOtherProfileStep(raw), /length/); + + const normalized = normalizeChartLibraryApiRecord(apiRecord("other-exact", exactRow)); + assert.doesNotThrow(() => pendingOther([normalized])); + assert.equal(pendingOther([normalized]), undefined); +});