Files
Jyotisha/frontend/tests/home-bootstrap-run.test.ts
T
Jesse_ChenandClaude Opus 5.5 b743b16b00 perf(home): city table, sealed-holdout JSON and the onboarding shell leave the first screen (BUG-1130)
The China city table loads on demand; old code-only profiles load it before
the people list settles, so they resolve exactly as before. The gate keeps
the six holdout fields it reads, pinned to the JSON by a test. The
onboarding shell is preloaded before reveal only when the profile is
incomplete.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-01 00:31:53 +08:00

373 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { ConsultationStatusError, createSession, LoginRedirectError } from "../src/lib/home-cloud-sync.ts";
import {
HOME_BOOTSTRAP_TIMEOUT_MESSAGE,
runHomeBootstrap,
type HomeBootstrapDeps,
type HomeBootstrapIo,
} from "../src/lib/home-bootstrap-run.ts";
import { HOME_BOOTSTRAP_SLOW_MS } from "../src/lib/home-bootstrap.ts";
import {
emptyProfile,
pendingConsultationStorageKey,
previewModelCatalog,
type Account,
type ChatSession,
type ConsultationStatus,
} from "../src/lib/home-types.ts";
import type { SessionListBoot } from "../src/lib/session-list-context.tsx";
import { bindCurrentSubjectAccount, readCurrentSubjectId, resetSubjectCatalogForTests, setCurrentSubject } from "../src/lib/current-subject.ts";
import { installWindow, withFakeTimers } from "./home-bootstrap-fake-timers.ts";
function installBrowserHosts() {
installWindow();
const host = globalThis as { localStorage?: Storage };
if (!host.localStorage) {
const data = new Map<string, string>();
host.localStorage = {
getItem: (key) => data.get(key) ?? null,
setItem: (key, value) => { data.set(key, String(value)); },
removeItem: (key) => { data.delete(key); },
clear: () => data.clear(),
key: (index) => Array.from(data.keys())[index] ?? null,
get length() { return data.size; },
};
}
}
function memoryStorage() {
const data = new Map<string, string>();
return {
getItem: (key: string) => data.get(key) ?? null,
removeItem: (key: string) => { data.delete(key); },
setItem: (key: string, value: string) => { data.set(key, value); },
};
}
function account(): Account {
return {
user: { id: "account-1", email: "reader@example.com" },
avatar: null,
credits: 3,
isAdmin: false,
adminUrl: null,
rectificationPriceCredits: 1,
activeSubscription: null,
hasConfirmedBirthTime: false,
hasUsableBirthTime: false,
profile: emptyProfile,
};
}
function createHarness(options: {
sessions: ChatSession[];
signedOut?: boolean;
io?: Partial<HomeBootstrapIo>;
restoreConsultationRecovery?: HomeBootstrapIo["restoreConsultationRecovery"];
} ) {
installBrowserHosts();
const state: {
account: Account | null;
error: string;
hydrated: boolean;
phase: string;
sessions: ChatSession[];
active: string;
notice: string;
profileSet: boolean;
catalogSet: boolean;
} = {
account: null,
error: "",
hydrated: false,
phase: "",
sessions: [] as ChatSession[],
active: "",
notice: "",
profileSet: false,
catalogSet: false,
};
const commits: string[] = [];
const storage = memoryStorage();
const boot: SessionListBoot = {
sessions: options.sessions,
rawRows: [],
cursor: "cursor-1",
account: options.signedOut ? null : account(),
signedOut: options.signedOut ?? false,
};
const note = (name: string) => { commits.push(name); };
const deps: HomeBootstrapDeps = {
sessionListReady: Promise.resolve(),
sessionListBoot: () => boot,
uiPreview: { current: false },
uiPreviewMode: { current: null },
consultationStatusMissingCount: { current: 0 },
isDevelopment: false,
readSearch: () => "",
storage,
restoreConsultationRecovery: options.restoreConsultationRecovery ?? (() => undefined),
setComposerNotice: (notice) => { state.notice = notice; note("notice"); },
commit: {
setAccount: (value) => { state.account = value; note("account"); },
setAccountError: (value) => { state.error = value; note("error"); },
setActiveSessionId: (id) => { state.active = id; note("active"); },
setBirthTimeAssessmentPhase: () => note("assessment"),
setBirthTimeJourney: () => note("journey"),
setBootstrapPhase: (phase) => { state.phase = phase; note("phase"); },
setGuidedJourneyPreview: () => note("preview"),
setHydrated: (value) => { state.hydrated = value; note("hydrated"); },
setModelCatalog: () => { state.catalogSet = true; note("catalog"); },
setOnboardingStep: () => note("onboarding"),
setProfile: () => { state.profileSet = true; note("profile"); },
setProfileDraft: () => note("draft"),
setSessions: (sessions) => { state.sessions = sessions; note("sessions"); },
setSessionsCursor: () => note("cursor"),
setStartGreeting: () => note("greeting"),
},
io: {
fetchModelCatalog: async () => previewModelCatalog,
fetchActiveConsultationStatus: async () => null,
// 原值:夹具不提供 preloadOnboardingShell / startEntrySummaryRead(io 当时没有这两项)。
// 新值:两者为空操作;断言不变。
// 原因:TASK-home-first-load T7/T1 新增的 io 项默认会真的 import 引导页组件、发校正入口请求,
// 夹具的 waitUntil 只让 30 个微任务,真实动态 import 等不到;本组测的是启动时序,不是这两项。
preloadOnboardingShell: async () => undefined,
startEntrySummaryRead: () => undefined,
fetchConsultationStatus: async () => {
throw new Error("unexpected status read");
},
resolveLookupBootstrap: async (input) => ({
selection: {
sessionId: input.defaultSessionId,
urlAction: "none",
missing: false,
},
sessions: input.sessions,
notice: null,
}),
...options.io,
},
};
return { deps, state, commits, storage };
}
async function waitUntil(predicate: () => boolean) {
for (let attempt = 0; attempt < 30; attempt += 1) {
if (predicate()) return;
await Promise.resolve();
}
assert.equal(predicate(), true);
}
test("a successful bootstrap fills the home once, after every read has settled", async () => {
const session = createSession(previewModelCatalog.defaultModelId);
let lookupStarted = false;
let releaseLookup: () => void = () => undefined;
const lookupGate = new Promise<void>((resolve) => { releaseLookup = resolve; });
const { deps, state, commits } = createHarness({
sessions: [session],
io: {
resolveLookupBootstrap: async (input) => {
lookupStarted = true;
await lookupGate;
return {
selection: {
sessionId: input.defaultSessionId,
urlAction: "none",
missing: false,
},
sessions: input.sessions,
notice: null,
};
},
},
});
const pending = runHomeBootstrap(deps, new AbortController().signal);
await waitUntil(() => lookupStarted);
assert.equal(state.account === null, true);
assert.equal(state.sessions.length, 0);
assert.equal(state.phase, "");
assert.equal(state.hydrated, false);
releaseLookup();
await pending;
assert.equal(state.account?.user.id, "account-1");
assert.equal(state.sessions[0]?.id, session.id);
assert.equal(state.active, session.id);
assert.equal(state.profileSet, true);
assert.equal(state.catalogSet, true);
assert.equal(state.phase, "prepare");
assert.equal(state.hydrated, false);
assert.equal(state.error, "");
assert.ok(commits.indexOf("account") < commits.indexOf("phase"));
});
test("a model catalog failure stays on the fatal screen and does not half-reveal", async () => {
const { deps, state } = createHarness({
sessions: [createSession(previewModelCatalog.defaultModelId)],
io: {
fetchModelCatalog: async () => { throw new Error("模型列表读取失败"); },
},
});
await runHomeBootstrap(deps, new AbortController().signal);
assert.equal(state.account, null);
assert.equal(state.sessions.length, 0);
assert.equal(state.catalogSet, false);
assert.equal(state.phase, "");
assert.equal(state.hydrated, true);
assert.equal(state.error, "模型列表读取失败");
});
test("a stored pending reservation wins over the global active status", async () => {
const older = createSession(previewModelCatalog.defaultModelId);
const pendingSession = createSession(previewModelCatalog.defaultModelId);
const globalStatus: ConsultationStatus = {
sessionId: older.id,
requestId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
status: "reserved",
};
const storedRequestId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
let restored: { sessionId: string; requestId: string } | null = null;
const { deps, storage } = createHarness({
sessions: [older, pendingSession],
restoreConsultationRecovery: (session, requestId) => {
restored = { sessionId: session.id, requestId };
},
io: {
fetchActiveConsultationStatus: async () => globalStatus,
fetchConsultationStatus: async (sessionId, requestId) => ({
sessionId,
requestId,
status: "reserved",
}),
},
});
storage.setItem(pendingConsultationStorageKey, JSON.stringify({
sessionId: pendingSession.id,
requestId: storedRequestId,
question: "稍后继续",
}));
await runHomeBootstrap(deps, new AbortController().signal);
assert.deepEqual(restored, { sessionId: pendingSession.id, requestId: storedRequestId });
assert.equal(deps.consultationStatusMissingCount.current, 0);
});
test("a stored 404 still reserves that request and counts the first miss", async () => {
const session = createSession(previewModelCatalog.defaultModelId);
const requestId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
let restored: string | null = null;
const { deps, storage } = createHarness({
sessions: [session],
restoreConsultationRecovery: (_session, restoredRequestId) => { restored = restoredRequestId; },
io: {
fetchActiveConsultationStatus: async () => null,
fetchConsultationStatus: async () => { throw new ConsultationStatusError(404, "missing"); },
},
});
storage.setItem(pendingConsultationStorageKey, JSON.stringify({
sessionId: session.id,
requestId,
question: "",
}));
await runHomeBootstrap(deps, new AbortController().signal);
assert.equal(deps.consultationStatusMissingCount.current, 1);
assert.equal(restored, requestId);
});
test("the slow-network timeout aborts the run and does not enter prepare", async () => {
installBrowserHosts();
await withFakeTimers(async (tick) => {
const { deps, state } = createHarness({
sessions: [createSession(previewModelCatalog.defaultModelId)],
io: {
fetchModelCatalog: (signal) => new Promise((_resolve, reject) => {
const fail = () => {
const error = new Error("Aborted");
error.name = "AbortError";
reject(error);
};
if (signal?.aborted) fail();
else signal?.addEventListener("abort", fail, { once: true });
}),
},
});
const pending = runHomeBootstrap(deps, new AbortController().signal);
await Promise.resolve();
tick(HOME_BOOTSTRAP_SLOW_MS);
await pending;
assert.equal(state.error, HOME_BOOTSTRAP_TIMEOUT_MESSAGE);
assert.equal(state.hydrated, true);
assert.equal(state.phase, "");
assert.equal(state.account, null);
});
});
test("a login redirect leaves the bootstrap timeout running", async () => {
installBrowserHosts();
await withFakeTimers(async (tick) => {
const { deps, state } = createHarness({
sessions: [],
signedOut: true,
io: {
redirectToLogin: () => { throw new LoginRedirectError(); },
},
});
await runHomeBootstrap(deps, new AbortController().signal);
assert.equal(state.error, "");
assert.equal(state.hydrated, false);
tick(HOME_BOOTSTRAP_SLOW_MS);
await Promise.resolve();
assert.equal(state.error, HOME_BOOTSTRAP_TIMEOUT_MESSAGE);
assert.equal(state.hydrated, true);
assert.equal(state.phase, "");
});
});
test("bootstrap waits for the current provider generation rather than an already settled initial gate", async () => {
const { deps, state } = createHarness({ sessions: [createSession(previewModelCatalog.defaultModelId)] });
let release!: () => void;
deps.waitForSessionList = () => new Promise(resolve => { release = resolve; });
const pending = runHomeBootstrap(deps, new AbortController().signal);
await waitUntil(() => Boolean(release));
assert.equal(state.hydrated, false);
release();
await pending;
assert.equal(state.phase, "prepare");
assert.equal(state.account?.user.id, "account-1");
assert.equal(state.error, "");
});
test("a catalog outage is a recoverable bootstrap error, never a login redirect", async () => {
let redirects = 0;
const { deps, state } = createHarness({ sessions: [], io: { redirectToLogin: () => { redirects += 1; throw new Error("unexpected login redirect"); } } });
deps.sessionListBoot = () => ({ sessions: [], rawRows: [], cursor: null, account: null, signedOut: false, error: "session_list_unavailable" });
await runHomeBootstrap(deps, new AbortController().signal);
assert.equal(redirects, 0);
assert.equal(state.hydrated, true);
assert.notEqual(state.error, "");
});
test("standard new=1 bootstrap creates a conversation bound to the selected person", async () => {
const { deps, state } = createHarness({ sessions: [], io: {
resolveLookupBootstrap: async input => ({
selection: { sessionId: input.defaultSessionId, urlAction: "new-chat", missing: false },
sessions: input.sessions, notice: null,
}),
writeSessionUrl: () => {}, clearLoginSessionReturn: () => {},
} });
resetSubjectCatalogForTests();
bindCurrentSubjectAccount("fictional-account", ["fictional-person"]);
setCurrentSubject("fictional-person");
deps.readSearch = () => "?new=1";
try {
await runHomeBootstrap(deps, new AbortController().signal);
assert.equal(state.sessions.find(session => session.id === state.active)?.chartProfileId, "fictional-person");
assert.equal(readCurrentSubjectId(), "fictional-person");
assert.equal(state.error, "");
} finally { resetSubjectCatalogForTests(); }
});