Files
Jyotisha/frontend/tests/home-warm-snapshot.test.ts
T
Jesse_ChenandClaude Opus 5.5 638b6a60c0 fix(home): warm return to / without replaying the loading ring (BUG-1040)
Coming back to / from /chart, /ephemeris, /reports or /people by client
navigation remounted Home from hydrated=false and replayed the whole
bootstrap: model catalog, consult status, entry summary and daily card,
3-5 round trips plus up to 4 s of reveal budget, every time.

The reveal now counts per document load, not per mount:
- lib/home-warm-snapshot.ts: memory-only, per-account snapshot of the
  model catalog, rectification entry summary, session cursor and today's
  card (keyed by person + birth fingerprint + day). Account, profile and
  the session list already survive in SessionListProvider. Cleared on
  account change, sign-out, provider 401 and redirectToLogin.
- lib/home-warm-start.ts: Home's useState initializers take one warm
  decision per mount. Complete snapshot + settled list + complete profile
  + a landing computable in memory -> start ready, landing resolved with
  the cold-path functions (?new=1, ?c=, login-return stash and its person
  scope from BUG-1038). Anything missing or needing a lookup -> the
  unchanged cold path (no half-reveal, BUG-1021).
- runHomeWarmRefresh commits the landing, then refreshes catalog, account
  and background-consultation recovery in the background (recovery shares
  resolveReservedConsultation with the cold path). Summary and daily card
  refresh through their existing effects.
- Next renders the new page before it writes the address bar, so AppLink /
  navigateAppPath note the target href (lib/client-navigation-target.ts);
  unknown target -> cold path.

Home() useState 33 / useRef 37 unchanged; page.tsx 1329 -> 1373 lines.

Tests: home-warm-return-lifecycle (13, real Home + sidebar + provider,
navigating in Next's render-then-write-URL order; 11 fail with warm start
disabled) and home-warm-snapshot (12). Full suite 3953 / 61 failing,
failure names identical to the 3928 / 61 baseline. Build: / stays Static,
rootMainFiles gzip 130933 B unchanged. Local Chrome with every /api/*
held 1.5 s: /chart -> 新建对话 interactive in 18-23 ms with no loading
ring (baseline 5530 ms with ring).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 08:21:35 +08:00

275 lines
17 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
clearHomeWarmSnapshot,
isCompleteHomeWarmSnapshot,
readHomeWarmDaily,
readHomeWarmSnapshot,
rememberHomeWarmDaily,
writeHomeWarmSnapshot,
type HomeWarmSnapshot,
} from "../src/lib/home-warm-snapshot.ts";
import {
applyWarmLanding,
dailyStarlanguageWarmKey,
reconcileSessionModels,
resolveHomeWarmStart,
resolveWarmLanding,
type HomeWarmStartInput,
} from "../src/lib/home-warm-start.ts";
import type { Account, ChatSession, Profile } from "../src/lib/home-types.ts";
// BUG-1040 T1: the warm snapshot store and the warm-start decision, as pure
// functions. Synthetic ids and profiles only.
const USER = "11111111-1111-4111-8111-111111111111";
const OTHER_USER = "22222222-2222-4222-8222-222222222222";
const CONS = "55555555-5555-4555-8555-555555555555";
const RECT = "33333333-3333-4333-8333-333333333333";
const PERSON = "66666666-6666-4666-8666-666666666666";
const catalog = { defaultModelId: "deepseek-pro", models: [{ id: "deepseek-pro", label: "DeepSeek V4 Pro", description: "x", creditCost: 1, isDefault: true }] };
const card = { trend: "虚构趋势", action: "虚构行动", caution: "虚构提醒" };
const selfProfile = {
name: "林遥", date: "1990-06-15", time: "12:30", reportedTime: "12:30", birthTimeSource: "legacy_import",
birthTimePeriod: "", declaredWindowStart: "", declaredWindowEnd: "", birthTimeClue: "",
uncertaintyBeforeMinutes: null, uncertaintyAfterMinutes: null, birthTimeStatus: "confirmed", rectificationCaseId: "",
countryCode: "CN", provinceCode: "110000", cityCode: "110000-city", districtCode: "110101", ayanamsa: "lahiri",
};
function snapshotFor(accountId: string, patch: Partial<HomeWarmSnapshot> = {}): HomeWarmSnapshot {
return { accountId, modelCatalog: catalog, entrySummary: null, entrySummarySettled: true, sessionsCursor: null, listBoot: null, ...patch };
}
function session(id: string, patch: Partial<ChatSession> = {}): ChatSession {
return {
id, title: "事业方向", theme: "career", modelId: "deepseek-pro", messages: [{ role: "user", text: "虚构问题" }],
createdAt: 1, updatedAt: 1, sessionType: "consultation", rectificationCaseId: null, pinned: false, archivedAt: null,
messagesHydrated: true, chartProfileId: "self", chartProfileName: "林遥", chartProfileRole: "self", ...patch,
};
}
function account(id = USER, profile: unknown = selfProfile): Account {
return {
user: { id, email: "fictional@example.test" }, profile, avatar: null, credits: 9, isAdmin: false, adminUrl: null,
rectificationPriceCredits: 1, activeSubscription: null, hasConfirmedBirthTime: true, hasUsableBirthTime: true,
} as unknown as Account;
}
function input(patch: Partial<HomeWarmStartInput> = {}): HomeWarmStartInput {
const boot = { sessions: [], rawRows: [], cursor: null, account: account(), signedOut: false };
return {
account: account(), settled: true, signedOut: false, boot, sessions: [session(CONS)], search: "?new=1",
storedReturnId: null, storedPendingRaw: null, subjectId: "self", subjectCatalog: [], previewRequested: false,
now: new Date("2026-09-26T04:00:00Z"), ...patch,
};
}
function withStorage<T>(run: () => T): T {
const values = new Map<string, string>();
const storage = { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => { values.set(key, value); }, removeItem: (key: string) => { values.delete(key); } };
const previous = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
Object.defineProperty(globalThis, "localStorage", { value: storage, configurable: true, writable: true });
try { return run(); } finally {
if (previous) Object.defineProperty(globalThis, "localStorage", previous);
else Reflect.deleteProperty(globalThis, "localStorage");
}
}
test("write → read returns the snapshot for the same account", () => {
clearHomeWarmSnapshot();
writeHomeWarmSnapshot(snapshotFor(USER));
assert.equal(readHomeWarmSnapshot(USER)?.accountId, USER);
assert.equal(isCompleteHomeWarmSnapshot(readHomeWarmSnapshot(USER)), true);
});
test("another account clears the snapshot instead of returning it", () => {
clearHomeWarmSnapshot();
writeHomeWarmSnapshot(snapshotFor(USER));
rememberHomeWarmDaily({ accountId: USER, subjectId: "self", fingerprint: "f", day: "2026-09-26", card });
assert.equal(readHomeWarmSnapshot(OTHER_USER), null);
assert.equal(readHomeWarmSnapshot(USER), null, "the previous account's snapshot is gone, not just hidden");
assert.equal(readHomeWarmDaily({ accountId: USER, subjectId: "self", fingerprint: "f", day: "2026-09-26" }), null);
writeHomeWarmSnapshot(snapshotFor(USER));
writeHomeWarmSnapshot(snapshotFor(OTHER_USER));
assert.equal(readHomeWarmSnapshot(USER), null);
});
test("a 401 login redirect clears the snapshot", async () => {
clearHomeWarmSnapshot();
writeHomeWarmSnapshot(snapshotFor(USER));
const { redirectToLogin, LoginRedirectError } = await import("../src/lib/home-cloud-sync.ts");
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
const previousSession = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage");
const replaced: string[] = [];
Object.defineProperty(globalThis, "window", { value: { location: { search: "", replace: (href: string) => replaced.push(href) } }, configurable: true, writable: true });
Object.defineProperty(globalThis, "sessionStorage", { value: { setItem() {} }, configurable: true, writable: true });
try {
assert.throws(() => redirectToLogin(), LoginRedirectError);
} finally {
if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow);
else Reflect.deleteProperty(globalThis, "window");
if (previousSession) Object.defineProperty(globalThis, "sessionStorage", previousSession);
else Reflect.deleteProperty(globalThis, "sessionStorage");
}
assert.deepEqual(replaced, ["/login"]);
assert.equal(readHomeWarmSnapshot(USER), null);
});
test("sign-out and the provider's signed-out and account-change branches clear the snapshot", () => {
const onboarding = readFileSync(new URL("../src/hooks/use-profile-onboarding.ts", import.meta.url), "utf8");
assert.match(onboarding, /clearHomeWarmSnapshot\(\);\n\s+await selfHostedOtpActions\.signOut\(\);\n\s+window\.location\.assign\("\/login"\);/);
const provider = readFileSync(new URL("../src/lib/session-list-context.tsx", import.meta.url), "utf8");
assert.match(provider, /accountResult\.status === 401\) \{\n\s+clearHomeWarmSnapshot\(\);/);
assert.match(provider, /sessionResponse\.status === 401\) \{\n\s+clearHomeWarmSnapshot\(\);/);
assert.match(provider, /readCurrentSubjectAccountId\(\) !== accountPayload\.user\.id\) \{\n(?:\s+\/\/.*\n)*\s+clearHomeWarmSnapshot\(\);/);
});
test("the snapshot lives in memory only", () => {
const source = readFileSync(new URL("../src/lib/home-warm-snapshot.ts", import.meta.url), "utf8");
const code = source.replace(/\/\*\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
assert.doesNotMatch(code, /localStorage|sessionStorage|indexedDB|document\.cookie/);
});
test("an incomplete snapshot is not a warm start", () => {
assert.equal(isCompleteHomeWarmSnapshot(null), false);
assert.equal(isCompleteHomeWarmSnapshot(snapshotFor(USER, { entrySummarySettled: false })), false);
assert.equal(isCompleteHomeWarmSnapshot(snapshotFor(USER, { modelCatalog: { defaultModelId: "", models: [] } })), false);
assert.equal(isCompleteHomeWarmSnapshot(snapshotFor("")), false);
});
test("today's card is keyed by person, fingerprint and day: another day or person refetches", () => {
clearHomeWarmSnapshot();
const profile = selfProfile as unknown as Profile;
const today = dailyStarlanguageWarmKey("self", profile, new Date("2026-09-26T04:00:00Z"));
assert.ok(today);
rememberHomeWarmDaily({ accountId: USER, subjectId: "self", ...today, card });
assert.deepEqual(readHomeWarmDaily({ accountId: USER, subjectId: "self", ...today }), card);
const tomorrow = dailyStarlanguageWarmKey("self", profile, new Date("2026-09-27T04:00:00Z"));
assert.ok(tomorrow);
assert.notEqual(tomorrow.day, today.day);
assert.equal(readHomeWarmDaily({ accountId: USER, subjectId: "self", ...tomorrow }), null, "a new day refetches");
const other = dailyStarlanguageWarmKey(PERSON, profile, new Date("2026-09-26T04:00:00Z"));
assert.ok(other);
assert.equal(readHomeWarmDaily({ accountId: USER, subjectId: PERSON, ...other }), null, "another person refetches");
assert.equal(readHomeWarmDaily({ accountId: OTHER_USER, subjectId: "self", ...today }), null);
assert.equal(dailyStarlanguageWarmKey("self", { ...profile, time: "", reportedTime: "", birthTimeStatus: "reported", birthTimeSource: "unknown" }, new Date()), null, "no birth minute → no card key");
});
test("warm start: complete snapshot + settled list → ready with a new empty chat for ?new=1", () => {
clearHomeWarmSnapshot();
writeHomeWarmSnapshot(snapshotFor(USER, { entrySummary: null, sessionsCursor: "cursor-a" }));
const start = withStorage(() => resolveHomeWarmStart(input()));
assert.ok(start, "warm start");
assert.equal(start.modelCatalog, catalog);
assert.equal(start.profile.name, "林遥");
assert.equal(start.onboardingStep, "name");
assert.ok(start.startGreeting.length > 0);
assert.equal(start.landing.selection.urlAction, "new-chat");
assert.ok(start.landing.pendingSession, "a local empty consultation");
assert.equal(start.landing.activeSessionId, start.landing.pendingSession.id);
assert.equal(start.landing.pendingSession.messages.length, 0);
assert.equal(start.landing.pendingSession.modelId, "deepseek-pro");
assert.equal(start.daily.kind, "pending", "no remembered card → static sentence, not a wait");
assert.equal(start.sessionsCursor, null, "the provider reloaded since the snapshot: its cursor wins");
const boot = { sessions: [], rawRows: [], cursor: "cursor-boot", account: account(), signedOut: false };
writeHomeWarmSnapshot(snapshotFor(USER, { sessionsCursor: "cursor-after-load-more", listBoot: boot }));
const same = withStorage(() => resolveHomeWarmStart(input({ boot })));
assert.equal(same?.sessionsCursor, "cursor-after-load-more", "same provider boot: the cursor Home last held");
});
test("warm start is refused whenever a piece is missing", () => {
clearHomeWarmSnapshot();
withStorage(() => {
assert.equal(resolveHomeWarmStart(input()), null, "no snapshot");
writeHomeWarmSnapshot(snapshotFor(USER));
assert.ok(resolveHomeWarmStart(input()));
assert.equal(resolveHomeWarmStart(input({ settled: false })), null, "list not settled");
assert.equal(resolveHomeWarmStart(input({ signedOut: true })), null, "signed out");
assert.equal(resolveHomeWarmStart(input({ account: null })), null, "no account");
assert.equal(resolveHomeWarmStart(input({ boot: { sessions: [], rawRows: [], cursor: null, account: null, signedOut: false, error: "session_list_unavailable" } })), null, "list error");
assert.equal(resolveHomeWarmStart(input({ previewRequested: true })), null, "development preview");
assert.equal(resolveHomeWarmStart(input({ account: account(USER, { name: "林遥" }) })), null, "onboarding profile");
assert.equal(resolveHomeWarmStart(input({ search: "?c=99999999-9999-4999-8999-999999999999" })), null, "a ?c= not in memory needs the cold lookup");
writeHomeWarmSnapshot(snapshotFor(USER, { entrySummarySettled: false }));
assert.equal(resolveHomeWarmStart(input()), null, "summary never settled");
writeHomeWarmSnapshot(snapshotFor(USER));
assert.equal(resolveHomeWarmStart(input({ account: account(OTHER_USER) })), null, "another account");
});
});
test("warm landing follows the cold rules: ?c= keeps, stash writes back, other person's stash is dropped", () => {
const scope = (subject: string) => (item: ChatSession) => (subject === "self" ? item.chartProfileId === "self" : item.chartProfileId === subject);
const create = () => session("new-local", { messages: [], title: "新对话" });
const rect = session(RECT, { sessionType: "birth_time_rectification", messages: [] });
const other = session(PERSON, { chartProfileId: PERSON, chartProfileRole: "other" });
const list = [rect, session(CONS), other];
const keep = resolveWarmLanding({ sessions: list, search: `?c=${CONS}`, storedReturnId: null, inSubjectScope: scope("self"), createHomeSession: create });
assert.equal(keep?.activeSessionId, CONS);
assert.equal(keep?.selection.urlAction, "keep");
assert.equal(keep?.pendingSession, null);
const stash = resolveWarmLanding({ sessions: list, search: "", storedReturnId: RECT, inSubjectScope: scope("self"), createHomeSession: create });
assert.equal(stash?.activeSessionId, RECT);
assert.equal(stash?.selection.urlAction, "replace-selected");
assert.equal(stash?.selection.clearStoredReturn, true);
const foreign = resolveWarmLanding({ sessions: list, search: "", storedReturnId: PERSON, inSubjectScope: scope("self"), createHomeSession: create });
assert.notEqual(foreign?.activeSessionId, PERSON, "another person's stash is not a place to return to (BUG-1038)");
assert.equal(foreign?.selection.urlAction, "none");
assert.equal(foreign?.selection.clearStoredReturn, true);
const bare = resolveWarmLanding({ sessions: list, search: "", storedReturnId: null, inSubjectScope: scope("self"), createHomeSession: create });
assert.equal(bare?.activeSessionId, CONS, "bare / on a rectification head lands on a consultation, as cold");
const newChat = resolveWarmLanding({ sessions: list, search: "?new=1", storedReturnId: RECT, inSubjectScope: scope("self"), createHomeSession: create });
assert.equal(newChat?.activeSessionId, "new-local");
assert.equal(newChat?.selection.clearStoredReturn, true, "the stash is ignored and cleared by a new chat");
const emptyPerson = resolveWarmLanding({ sessions: list, search: "?new=1", storedReturnId: null, inSubjectScope: scope("nobody"), createHomeSession: create });
assert.equal(emptyPerson?.activeSessionId, "new-local", "a person with no sessions gets exactly one local chat");
assert.equal(resolveWarmLanding({ sessions: list, search: "", storedReturnId: "99999999-9999-4999-8999-999999999999", inSubjectScope: scope("self"), createHomeSession: create }), null, "a stash not in memory needs the cold lookup");
});
test("applying the landing replaces older unsaved empty chats; model reconcile mirrors readSessions", () => {
const oldEmpty = session("old-empty", { messages: [] });
const pending = session("new-local", { messages: [] });
const landing = { activeSessionId: pending.id, pendingSession: pending, selection: { sessionId: pending.id, urlAction: "new-chat" as const, missing: false, clearStoredReturn: true } };
const next = applyWarmLanding([oldEmpty, session(CONS)], landing);
assert.deepEqual(next.map((item) => item.id), ["new-local", CONS]);
const same = [pending, session(CONS)];
assert.equal(applyWarmLanding(same, landing), same, "already applied → unchanged");
const unchanged = [session(CONS)];
const kept = reconcileSessionModels(unchanged, catalog);
assert.equal(kept.sessions, unchanged, "no fallback keeps identity (no re-render)");
const offline = reconcileSessionModels([session(CONS, { modelId: "retired-model" })], catalog);
assert.deepEqual(offline.fallbackSessionIds, [CONS]);
assert.equal(offline.sessions[0].modelId, "deepseek-pro");
});
test("the query of the / being opened: location when already there, else the noted AppLink href, else unknown", async () => {
const { homeSearchForMount } = await import("../src/lib/home-warm-start.ts");
const { noteClientNavigationTarget, takeClientNavigationTarget } = await import("../src/lib/client-navigation-target.ts");
const origin = "http://localhost";
// Popstate / document load: the address bar already shows `/`.
assert.equal(homeSearchForMount({ pathname: "/", search: "?c=abc", origin }, "/chart"), "?c=abc");
// Client navigation from /chart: Next writes the URL after rendering.
assert.equal(homeSearchForMount({ pathname: "/chart", search: "", origin }, "/?new=1"), "?new=1");
assert.equal(homeSearchForMount({ pathname: "/reports", search: "", origin }, `/?c=${CONS}`), `?c=${CONS}`);
assert.equal(homeSearchForMount({ pathname: "/chart", search: "", origin }, "/"), "");
assert.equal(homeSearchForMount({ pathname: "/chart", search: "", origin }, null), null, "nothing noted → cold path");
assert.equal(homeSearchForMount({ pathname: "/chart", search: "", origin }, "/ephemeris"), null);
noteClientNavigationTarget("/?new=1");
assert.equal(takeClientNavigationTarget(), "/?new=1");
assert.equal(takeClientNavigationTarget(), null, "taken once");
noteClientNavigationTarget("/?new=1");
assert.equal(takeClientNavigationTarget(Date.now() + 60_000), null, "a stale note is not trusted");
noteClientNavigationTarget("https://example.test/");
assert.equal(takeClientNavigationTarget(), null, "only same-origin paths");
});