import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import React, { useLayoutEffect } from "react"; import test from "node:test"; import { useChartPage } from "../src/hooks/use-chart-page.ts"; import { assembleChartView } from "../src/lib/chart-view-load.ts"; import type { ChartViewLayer, EngineCallResult } from "../src/lib/chart-view-engine.ts"; import type { ChartViewOk } from "../src/lib/chart-view-contract.ts"; import { CHART_VIEW_CLIENT_TIMEOUT_MS, CHART_VIEW_COPY } from "../src/lib/chart-view-labels.ts"; import { peekChartPage, pinChartSnapshotIdentity, prefetchSecondaryPage, resetSecondaryPageDataForTests, writeChartPage } from "../src/lib/secondary-page-data.ts"; import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts"; const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8")); const profile = { name: "Synthetic", date: "1990-06-15", time: "12:00", placeLabel: "Synthetic", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed" }; async function packet(layer?: ChartViewLayer, failure?: EngineCallResult) { return assembleChartView({ userId: "synthetic", accountId: "synthetic", profileFingerprint: "fixture-v1", profile, asOf: "2026-09-15", layers: layer ? [layer] : [], postEngine: async (path) => { if (path === "/api/chart") return { status: "ok", payload: golden.chart }; if (failure) return failure; const payload = path === "/api/varga_full" ? golden.varga_full : path === "/api/dasha/chara" ? golden.chara : golden.western; return { status: "ok", payload }; } }); } function response(result: Awaited>) { return new Response(JSON.stringify(result.body), { status: result.httpStatus }); } function deferred() { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; const promise = new Promise((a, b) => { resolve = a; reject = b; }); return { promise, resolve, reject }; } async function harness(run: (h: ReturnType, current: () => ReturnType, assigned: string[]) => Promise) { resetSecondaryPageDataForTests(); const originalFetch = globalThis.fetch; const originalTimeout = AbortSignal.timeout; const h = createClientLifecycleHarness(); let value!: ReturnType; const assigned: string[] = []; Object.assign(window, { location: { assign: (url: string) => { assert.equal(value.view?.status, "unauthenticated"); assigned.push(url); } } }); function Probe() { const state = useChartPage(); useLayoutEffect(() => { value = state; }); return null; } const mount = h.render.bind(h); h.render = () => mount(React.createElement(Probe)); try { await run(h, () => value, assigned); assert.deepEqual(h.errors, []); } finally { await h.close(); globalThis.fetch = originalFetch; AbortSignal.timeout = originalTimeout; resetSecondaryPageDataForTests(); } } test("chart first request times out with client_timeout and timedOut copy", async () => harness(async (h, state) => { const deadline = new AbortController(); AbortSignal.timeout = (ms) => { assert.equal(ms, CHART_VIEW_CLIENT_TIMEOUT_MS); return deadline.signal; }; globalThis.fetch = async (_url, init) => new Promise((_resolve, reject) => init?.signal?.addEventListener("abort", () => reject(init.signal!.reason), { once: true })); await h.render(null); assert.equal(state().view, null); await h.update(() => deadline.abort(new DOMException("synthetic", "TimeoutError"))); assert.equal(state().view?.status, "client_timeout"); const view = state().view; assert.equal(view?.status !== "ok" && view?.message, CHART_VIEW_COPY.timedOut); })); test("sidebar prefetch shares the same bounded request with the mounted hook", async () => harness(async (h, state) => { const deadline = new AbortController(); let calls = 0; AbortSignal.timeout = (ms) => { assert.equal(ms, 25_000); return deadline.signal; }; globalThis.fetch = async (_url, init) => { calls++; return new Promise((_resolve, reject) => init?.signal?.addEventListener("abort", () => reject(init.signal!.reason))); }; prefetchSecondaryPage("/chart"); await h.render(null); await h.update(() => deadline.abort(new DOMException("synthetic", "TimeoutError"))); assert.equal(calls, 1); assert.equal(state().view?.status, "client_timeout"); })); test("chart cache populated during failed fetch is painted instead of remaining null", async () => harness(async (h, state) => { const request = deferred(); globalThis.fetch = () => request.promise; await h.render(null); const result = await packet(); writeChartPage({ kind: "view", view: result.body }); await h.update(() => request.reject(new TypeError("synthetic network failure"))); assert.deepEqual(state().view, result.body); })); for (const cached of [false, true]) test(`chart 401 ${cached ? "cache" : "response"} paints unauthenticated before redirect`, async () => harness(async (h, state, assigned) => { globalThis.fetch = async () => new Response("{}", { status: 401 }); if (cached) writeChartPage({ kind: "unauthenticated" }); await h.render(null); assert.equal(state().view?.status, "unauthenticated"); assert.deepEqual(assigned, ["/login"]); })); for (const layer of ["varga", "chara", "western", "qizheng"] as const) test(`real assemble to fetch to hook preserves ${layer} engine 429`, async () => harness(async (h, state) => { const initial = await packet(); const failed = await packet(layer, { status: "busy", path: "/api/synthetic", elapsedMs: 1, httpStatus: 429 }); globalThis.fetch = async (url) => response(String(url).includes("layers=") ? failed : initial); await h.render(null); await h.update(() => state().requestLayer(layer)); assert.equal(state().layerFailures.get(layer), "engine_busy"); assert.equal(state().pendingLayers.has(layer), false); assert.deepEqual(state().view, initial.body); })); test("layer catch fails unavailable and a retry clears failure while pending then succeeds", async () => harness(async (h, state) => { const initial = await packet(); const good = await packet("varga"); const retry = deferred(); let calls = 0; globalThis.fetch = async (url) => { if (!String(url).includes("layers=")) return response(initial); calls++; if (calls === 1) throw new TypeError("synthetic"); return retry.promise; }; await h.render(null); await h.update(() => state().requestLayer("varga")); assert.equal(state().layerFailures.get("varga"), "unavailable"); await h.update(() => { state().requestLayer("varga"); state().requestLayer("varga"); }); assert.equal(state().layerFailures.has("varga"), false); assert.equal(state().pendingLayers.has("varga"), true); assert.equal(calls, 2); await h.update(() => retry.resolve(response(good))); assert.equal(state().pendingLayers.has("varga"), false); assert.equal(state().layerFailures.size, 0); assert.ok((state().view as ChartViewOk).vedic.vargas.some((v) => v.id === "D9")); })); test("BFF rate_limited stays distinct from engine_busy", async () => harness(async (h, state) => { const initial = await packet(); globalThis.fetch = async (url) => String(url).includes("layers=") ? new Response(JSON.stringify({ status: "rate_limited", billed: false, message: CHART_VIEW_COPY.rateLimited }), { status: 429 }) : response(initial); await h.render(null); await h.update(() => state().requestLayer("varga")); assert.equal(state().layerFailures.get("varga"), "rate_limited"); })); test("layer timeout while reading response JSON remains client_timeout", async () => harness(async (h, state) => { const initial = await packet(); const deadline = new AbortController(); globalThis.fetch = async (url) => String(url).includes("layers=") ? { status: 200, json: () => new Promise((_resolve, reject) => deadline.signal.addEventListener("abort", () => reject(new DOMException("synthetic", "AbortError")))) } as Response : response(initial); await h.render(null); AbortSignal.timeout = () => deadline.signal; await h.update(() => state().requestLayer("varga")); await h.update(() => deadline.abort(new DOMException("synthetic", "TimeoutError"))); assert.equal(state().layerFailures.get("varga"), "client_timeout"); })); test("out-of-order successful layers preserve previously loaded data", async () => harness(async (h, state) => { const initial = await packet(); const varga = await packet("varga"); const western = await packet("western"); const v = deferred(); const w = deferred(); globalThis.fetch = async (url) => String(url).includes("varga") ? v.promise : String(url).includes("western") ? w.promise : response(initial); await h.render(null); await h.update(() => { state().requestLayer("varga"); state().requestLayer("western"); }); await h.update(() => w.resolve(response(western))); await h.update(() => v.resolve(response(varga))); const view = state().view as ChartViewOk; assert.equal(view.western.status, "ok"); assert.ok(view.vedic.vargas.some((x) => x.id === "D9")); })); test("late warm D1 refresh preserves the layer that completed after mounting", async () => harness(async (h, state) => { const initial = await packet(); const western = await packet("western"); const refresh = deferred(); writeChartPage({ kind: "view", view: initial.body }); globalThis.fetch = async (url) => String(url).includes("layers=") ? response(western) : refresh.promise; await h.render(null); await h.update(() => state().requestLayer("western")); assert.equal((state().view as ChartViewOk).western.status, "ok"); await h.update(() => refresh.resolve(response(initial))); assert.equal((state().view as ChartViewOk).western.status, "ok"); })); test("HTTP 429 without a valid body remains engine_busy", async () => harness(async (h, state) => { const initial = await packet(); globalThis.fetch = async (url) => String(url).includes("layers=") ? new Response("busy", { status: 429 }) : response(initial); await h.render(null); await h.update(() => state().requestLayer("western")); assert.equal(state().layerFailures.get("western"), "engine_busy"); })); test("an obsolete first fetch failure cannot overwrite a new profile snapshot", async () => harness(async (h, state) => { const old = deferred(); const initial = await packet(); const fresh = { ...initial, body: { ...initial.body, profileFingerprint: "fixture-v2" } }; let calls = 0; globalThis.fetch = async () => ++calls === 1 ? old.promise : response(fresh); await h.render(null); pinChartSnapshotIdentity({ accountId: "synthetic", fingerprint: "fixture-v2" }); await h.update(() => old.reject(new DOMException("synthetic", "TimeoutError"))); assert.equal(state().view?.profileFingerprint, "fixture-v2"); assert.equal(peekChartPage()?.kind === "view" && peekChartPage()?.kind, "view"); assert.equal(calls, 2); })); test("layer 401 paints failure before redirect and never becomes a layer wait", async () => harness(async (h, state, assigned) => { const initial = await packet(); globalThis.fetch = async (url) => String(url).includes("layers=") ? new Response(null, { status: 401 }) : response(initial); await h.render(null); await h.update(() => state().requestLayer("western")); assert.deepEqual(assigned, ["/login"]); assert.equal(state().view?.status, "unauthenticated"); assert.equal(state().pendingLayers.size, 0); })); test("profile pin invalidation ignores an older layer completion", async () => harness(async (h, state) => { const initial = await packet(); const old = await packet("varga"); const request = deferred(); globalThis.fetch = async (url) => String(url).includes("layers=") ? request.promise : response(initial); await h.render(null); await h.update(() => state().requestLayer("varga")); pinChartSnapshotIdentity({ accountId: "synthetic", fingerprint: "fixture-v2" }); const fresh = { ...initial.body, profileFingerprint: "fixture-v2" }; writeChartPage({ kind: "view", view: fresh }); await h.update(() => request.resolve(response(old))); assert.deepEqual(peekChartPage(), { kind: "view", view: fresh }); assert.equal(state().layerFailures.size, 0); }));