Files
Jyotisha/frontend/tests/home-bootstrap-run.test.ts
T
Jesse_ChenandClaude Opus 5.5 55ebdd7a95
Independent Staging Quality Gate / publish (push) Canceled after 0s
Independent Staging Quality Gate / validate (push) Canceled after 9m27s
Home first screen no longer carries the rectification surface or react-markdown (issue #124, BUG-1474)
page.tsx imported the whole rectification surface statically; its verification
report also imported chat-markdown-view statically, which pulled react-markdown
into / even though chat-message-content already loaded it lazily.

- Home renders a same-named lazy wrapper; openRectificationCase starts the
  download with the open request and awaits it before setting the ids that
  mount the surface, so it still reveals once, drawn.
- The markdown loader moves to lib/chat-markdown-loader; the home bootstrap
  awaits it before revealing a conversation with messages (no plain-text flash
  on ?c=). The blank home does not wait.

/ first load, gzip-9 of every referenced JS/CSS: 704,165 -> 609,606 B (-13.4%),
still Static.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:25:43 +08:00

420 lines
16 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,
// 原值:夹具不提供 preloadChatMarkdown(io 当时没有这一项)。
// 新值:空操作;原因同上:默认会真的 import react-markdown(issue #124)。
preloadChatMarkdown: 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 ?c= conversation with messages reveals only after markdown has loaded (issue #124)", async () => {
const session = {
...createSession(previewModelCatalog.defaultModelId),
messages: [{ role: "user" as const, text: "这个月工作怎么样?" }, { role: "assistant" as const, text: "**稳住**节奏。" }],
messagesHydrated: true,
};
let preloads = 0;
let releaseMarkdown: () => void = () => undefined;
const markdownGate = new Promise<void>((resolve) => { releaseMarkdown = resolve; });
const { deps, state } = createHarness({
sessions: [session],
io: {
preloadChatMarkdown: () => { preloads += 1; return markdownGate; },
resolveLookupBootstrap: async (input) => ({
selection: { sessionId: session.id, urlAction: "keep", missing: false },
sessions: input.sessions,
notice: null,
}),
},
});
const pending = runHomeBootstrap(deps, new AbortController().signal);
await waitUntil(() => preloads === 1);
for (let tick = 0; tick < 30; tick += 1) await Promise.resolve();
assert.equal(state.sessions.length, 0, "the conversation is not committed before markdown arrives");
releaseMarkdown();
await pending;
assert.equal(state.active, session.id);
assert.equal(preloads, 1);
});
test("the blank starter home does not wait for markdown (issue #124)", async () => {
let preloads = 0;
const { deps, state } = createHarness({
sessions: [createSession(previewModelCatalog.defaultModelId)],
io: {
preloadChatMarkdown: () => { preloads += 1; return new Promise<void>(() => undefined); },
},
});
await runHomeBootstrap(deps, new AbortController().signal);
assert.ok(state.active, "the home was committed");
assert.ok(state.sessions.length > 0);
assert.equal(preloads, 0);
});
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(); }
});