From fabe0114dd324cf56e4690d1be7c16d42916af07 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 25 Sep 2026 04:51:49 +0800 Subject: [PATCH] fix(chart): keep subject-scoped failures visible and follow the new routes A 401 for the current person was stored, then hidden because the subject stamp only happened on success. Session, report, and birth contracts now follow the subject query and the shared birth lookup. Admin reset fixtures can no longer insert a retired self mirror. --- frontend/src/lib/secondary-page-data.ts | 15 ++++++++++++--- frontend/tests/chart-page-view.test.tsx | 2 +- frontend/tests/chart-view-route.test.ts | 3 ++- frontend/tests/consultation-entrypoint.test.ts | 2 +- .../tests/database-admin-account-reset.test.ts | 3 ++- frontend/tests/home-bootstrap-reveal.test.ts | 3 ++- frontend/tests/personal-report-api.test.ts | 4 ++-- frontend/tests/report-row-delete.test.tsx | 2 +- frontend/tests/secondary-page-entry.test.ts | 2 +- frontend/tests/server-owned-birth-profile.test.ts | 2 +- frontend/tests/session-list-provider.test.ts | 2 +- frontend/tests/settings-mvp-contract.test.ts | 3 ++- 12 files changed, 28 insertions(+), 15 deletions(-) diff --git a/frontend/src/lib/secondary-page-data.ts b/frontend/src/lib/secondary-page-data.ts index badd02e1..cbc060ff 100644 --- a/frontend/src/lib/secondary-page-data.ts +++ b/frontend/src/lib/secondary-page-data.ts @@ -255,7 +255,10 @@ const ephemerisCache = createMemoryCache(async () => { headers: { Accept: "application/json" }, cache: "no-store", }); - if (response.status === 401) return { kind: "unauthorized" }; + if (response.status === 401) { + ephemerisSubject = subjectId; + return { kind: "unauthorized" }; + } const json: unknown = await response.json().catch(() => null); const payload = parseEphemerisOkResponse(json); if (!payload) throw new Error("ephemeris_page_bad_payload"); @@ -304,8 +307,14 @@ const reportsCache = createMemoryCache(async () => { }); const json: unknown = await response.json().catch(() => null); if (readCurrentSubjectId() !== subjectId) throw new Error("reports_subject_changed"); - if (response.status === 401) return { kind: "unauthorized" }; - if (!response.ok) return { kind: "error", reports: [] }; + if (response.status === 401) { + reportsSubject = subjectId; + return { kind: "unauthorized" }; + } + if (!response.ok) { + reportsSubject = subjectId; + return { kind: "error", reports: [] }; + } reportsSubject = subjectId; return { kind: "ready", reports: readReports(json) }; }); diff --git a/frontend/tests/chart-page-view.test.tsx b/frontend/tests/chart-page-view.test.tsx index 96790077..1d384c36 100644 --- a/frontend/tests/chart-page-view.test.tsx +++ b/frontend/tests/chart-page-view.test.tsx @@ -442,6 +442,6 @@ test("the (app) layout mounts one sidebar for home and the four secondary routes assert.equal(layout.match(/ { } assert.match(routeSource, /export async function GET/); assert.match(serviceSource, /createServerSupabaseClient/); - assert.match(serviceSource, /ACCOUNT_BIRTH_SELECT/); + assert.match(serviceSource, /loadSubjectBirth/); + assert.doesNotMatch(serviceSource, /clientBirth|body\.profile/); assert.match(loadSource, /\/api\/chart/); assert.match(loadSource, /\/api\/dasha\/chara/); assert.match(loadSource, /\/api\/varga_full/); diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts index 858ea9c2..06daac3f 100644 --- a/frontend/tests/consultation-entrypoint.test.ts +++ b/frontend/tests/consultation-entrypoint.test.ts @@ -320,7 +320,7 @@ test("rectification mutations report pending state while session-level return co // 新值:onPendingChange: setRectificationMutationPending 写在 hook 的 panel // 原因:mutationPending 仍由外壳对象持有,但 setter 经 panel 交给子树 assert.match(source, /onPendingChange: setRectificationMutationPending/); - assert.match(source, /disabled=\{productEntrypointsDisabled \|\| rectificationLoading \|\| rectificationMutationPending\}/); + assert.match(source, /disabled=\{productEntrypointsDisabled \|\| rectificationLoading \|\| rectificationMutationPending \|\| !rectificationForSelf\}/); assert.doesNotMatch(source, /重试恢复/); assert.doesNotMatch(source, /返回并恢复原问题|返回首页/); }); diff --git a/frontend/tests/database-admin-account-reset.test.ts b/frontend/tests/database-admin-account-reset.test.ts index 23b27a25..b2d8d120 100644 --- a/frontend/tests/database-admin-account-reset.test.ts +++ b/frontend/tests/database-admin-account-reset.test.ts @@ -82,8 +82,9 @@ test("admin customer reset clears only rebuildable application state", async () insert into public.chat_sessions (user_id, title, theme, messages) values ('${targetId}', 'Reset Chat', 'general', '[]'::jsonb); + -- 原值: role self。新值: other。原因: 本人镜像已停写,重置仍要清掉可重建的他人档案。 insert into public.chart_profiles (user_id, role, profile) - values ('${targetId}', 'self', '{}'::jsonb); + values ('${targetId}', 'other', '{}'::jsonb); insert into public.synastry_reports (user_id, partner_name, report) values ('${targetId}', 'Partner', '{}'::jsonb); insert into public.credit_transactions (user_id, transaction_type, amount, balance_after, request_id) diff --git a/frontend/tests/home-bootstrap-reveal.test.ts b/frontend/tests/home-bootstrap-reveal.test.ts index 571f2476..f5c2a0e5 100644 --- a/frontend/tests/home-bootstrap-reveal.test.ts +++ b/frontend/tests/home-bootstrap-reveal.test.ts @@ -162,7 +162,8 @@ test("bootstrap enters the prepare phase instead of revealing after the account // 原值: 3 个 prepare 守卫(onboarding / 今日星语 / 入口摘要)。 // 新值: 2 个(今日星语 / 入口摘要)。 // 原因: 建议问题请求已删除。 - const guards = page.match(/if \(bootstrapPhase === "account" \|\| !accountId/g) ?? []; + const binder = readFileSync(new URL("../src/components/daily-starlanguage-binder.tsx", import.meta.url), "utf8"); + const guards = `${page}\n${binder}`.match(/if \(bootstrapPhase === "account" \|\| !accountId/g) ?? []; assert.equal(guards.length, 2, "daily starlanguage and entry summary must both key off bootstrapPhase"); assert.match(page, /setRectificationEntrySummarySettled\(true\);/); assert.match(page, /bootstrapRevealDelayMs\(Date\.now\(\), prepareStartedAt\.current, bootstrapPrepareReady\)/); diff --git a/frontend/tests/personal-report-api.test.ts b/frontend/tests/personal-report-api.test.ts index 9cf89c07..36472b26 100644 --- a/frontend/tests/personal-report-api.test.ts +++ b/frontend/tests/personal-report-api.test.ts @@ -1298,7 +1298,7 @@ function seedRecord(): PersonalReportRecord { test("POST route uses dual clients: authenticated reads + admin persistence", () => { assert.match(createRoute, /createServerSupabaseClient\(\)/); - assert.match(createRoute, /\.from\("profiles"\)/); + assert.match(createRoute, /loadSubjectBirth/); assert.match(createRoute, /createAdminSupabaseClient\(\)/); assert.match(createRoute, /createSupabasePersonalReportService\(admin\)/); assert.match(createRoute, /createPersonalReportDataClient\(admin\)/); @@ -1358,7 +1358,7 @@ test("POST enqueues durable work without Next.js after and GET lists metadata wi assert.match(createRoute, /jobs:\s*createSupabasePersonalReportJobService\(admin\)/); assert.doesNotMatch(createRoute, /\bafter\s*\(/); assert.doesNotMatch(createRoute, /deferGeneration/); - assert.match(createRoute, /export async function GET\(\)/); + assert.match(createRoute, /export async function GET\(request: Request\)/); assert.match(createRoute, /REPORT_LIST_COLUMNS/); const listColumns = createRoute.slice( createRoute.indexOf("const REPORT_LIST_COLUMNS"), diff --git a/frontend/tests/report-row-delete.test.tsx b/frontend/tests/report-row-delete.test.tsx index 4f8ed510..4fff53f2 100644 --- a/frontend/tests/report-row-delete.test.tsx +++ b/frontend/tests/report-row-delete.test.tsx @@ -80,7 +80,7 @@ test("mounted report center removes deleted row and an older GET cannot resurrec writeReportsPage({ kind: "ready", reports: [row] }); globalThis.fetch = async (url, options) => { if (options?.method === "DELETE") return new Response(null, { status: 204 }); - if (url === "/api/reports") { gets += 1; return stale.promise; } + if (String(url).startsWith("/api/reports")) { gets += 1; return stale.promise; } return Response.json({}, { status: 401 }); }; try { diff --git a/frontend/tests/secondary-page-entry.test.ts b/frontend/tests/secondary-page-entry.test.ts index 4b587699..d24dbf8b 100644 --- a/frontend/tests/secondary-page-entry.test.ts +++ b/frontend/tests/secondary-page-entry.test.ts @@ -97,7 +97,7 @@ test("a warm chart cache does not go through a null waiting state", async () => assert.notEqual(warm, null); const secondMountView = peekChartPage()?.kind === "view" ? peekChartPage() : null; assert.equal(secondMountView?.kind, "view"); - assert.match(chartHook, /useState\(\(\) => viewFromSnapshot\(peekChartPage\(\)\)\)/); + assert.match(chartHook, /useState\(\(\) => viewMatchingSubject\(subjectId\)\)/); }); test("chart / ephemeris / reports caches share one inflight fetch and keep the last result", async () => { diff --git a/frontend/tests/server-owned-birth-profile.test.ts b/frontend/tests/server-owned-birth-profile.test.ts index 713b264f..8d98064f 100644 --- a/frontend/tests/server-owned-birth-profile.test.ts +++ b/frontend/tests/server-owned-birth-profile.test.ts @@ -91,7 +91,7 @@ test("daily and synastry routes share a literal account-birth select string", () const subjectBirth = readFileSync(new URL("../src/lib/subject-birth.ts", import.meta.url), "utf8"); assert.match(subjectBirth, /select\(ACCOUNT_BIRTH_SELECT\)/); assert.match(daily, /loadSubjectBirth/); - assert.match(synastry, /select\(ACCOUNT_BIRTH_SELECT\)/); + assert.match(synastry, /loadSubjectBirth/); assert.doesNotMatch(daily, /accountBirthColumns/); assert.doesNotMatch(synastry, /accountBirthColumns/); assert.doesNotMatch(daily, /\.join\(",\"\)/); diff --git a/frontend/tests/session-list-provider.test.ts b/frontend/tests/session-list-provider.test.ts index fafc65f1..6fed7acb 100644 --- a/frontend/tests/session-list-provider.test.ts +++ b/frontend/tests/session-list-provider.test.ts @@ -18,7 +18,7 @@ test("the app layout owns one SessionListProvider and one SidebarProvider", () = }); test("the provider fetches the session list once and home does not fetch it again", () => { - assert.match(provider, /fetch\(`\/api\/sessions\?limit=\$\{SESSION_PAGE_SIZE\}`/); + assert.match(provider, /fetch\(`\/api\/sessions\?limit=\$\{SESSION_PAGE_SIZE\}&subject=\$\{encodeURIComponent\(readCurrentSubjectId\(\)\)\}`/); assert.match(provider, /fetch\("\/api\/account"/); // 原值: page.tsx 里 `await sessionListReady;` // 新值: page.tsx 把 sessionListReady 交给 runHomeBootstrap,函数里 `await deps.sessionListReady`。 diff --git a/frontend/tests/settings-mvp-contract.test.ts b/frontend/tests/settings-mvp-contract.test.ts index ebd2e564..dd4df7a0 100644 --- a/frontend/tests/settings-mvp-contract.test.ts +++ b/frontend/tests/settings-mvp-contract.test.ts @@ -8,7 +8,8 @@ const overlay = readFileSync(new URL("../src/components/account-dialog-overlay.t test("account menu exposes the four MVP settings sections", () => { assert.match(sidebar, /个人资料<\/span>/); - assert.match(sidebar, /星盘资料<\/span>/); + assert.match(sidebar, /href: "\/people", label: "星盘档案"/); + assert.doesNotMatch(sidebar, /星盘资料<\/span>/); assert.match(sidebar, /通用设置<\/span>/); assert.match(sidebar, /账户与点数<\/span>/); assert.match(sidebar, /onOpenGeneral/);