Files
Jyotisha/frontend/tests/home-bootstrap-run.test.ts
T
jesse-uxandClaude Code 4d801e53c3
Independent Staging Quality Gate / validate (push) Successful in 16m28s
Independent Staging Quality Gate / publish (push) Successful in 3m33s
fix: integrate people archive, report reader and western chart corrections
Validate on Linux Node 22 and PostgreSQL 17: 3894 frontend tests and 64 database tests pass, with no removed test names or new failures. Preserve static Home, bounded gzip, assertion-change records and manual acceptance gaps.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 20:41:16 +08:00

369 lines
13 KiB
TypeScript

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,
fetchConsultationStatus: async () => {
throw new Error("unexpected status read");
},
resolveLookupBootstrap: async (input) => ({
selection: {
sessionId: input.defaultSessionId,
urlAction: "none",
missing: false,
clearStoredReturn: 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,
clearStoredReturn: 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, clearStoredReturn: 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(); }
});