import assert from "node:assert/strict"; import test from "node:test"; import React, { useInsertionEffect, useLayoutEffect, type ReactNode } from "react"; import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts"; Object.assign(globalThis, { React }); // BUG-1040. Real React lifecycle: the real Home, the real read-only sidebar and // the real SessionListProvider composed like app/(app)/layout.tsx. A client // navigation swaps the route child (Home unmounts and remounts) while the // provider stays; a document load resets module state and keeps storages. // The claim under test: the loading ring plays once per document load, and a // client return to `/` is ready in its first committed frame. // Synthetic ids and profiles only (no real person data). const USER = "11111111-1111-4111-8111-111111111111"; const OTHER_USER = "22222222-2222-4222-8222-222222222222"; const RECT = "33333333-3333-4333-8333-333333333333"; const CASE = "44444444-4444-4444-8444-444444444444"; const CONS = "55555555-5555-4555-8555-555555555555"; const REQUEST = "77777777-7777-4777-8777-777777777777"; 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", }; const rectRow = { id: RECT, title: "生时校正 · 04:45–05:15", theme: "general", model_id: "deepseek-pro", messages: [], session_type: "birth_time_rectification", rectification_case_id: CASE, chart_profile_id: "self", chart_profile_name: "林遥", chart_profile_role: "self", pinned: false, archived_at: null, created_at: "2026-09-20T00:00:00Z", updated_at: "2026-09-25T00:00:00Z", }; const consultationRow = { id: CONS, title: "事业方向", theme: "career", model_id: "deepseek-pro", messages: [{ role: "user", text: "虚构问题" }, { role: "assistant", text: "虚构回答" }], session_type: "consultation", rectification_case_id: null, chart_profile_id: "self", chart_profile_name: "林遥", chart_profile_role: "self", pinned: false, archived_at: null, created_at: "2026-09-01T00:00:00Z", updated_at: "2026-09-01T00:00:00Z", }; const catalogPro = { defaultModelId: "deepseek-pro", models: [{ id: "deepseek-pro", label: "DeepSeek V4 Pro", description: "x", creditCost: 1, isDefault: true }] }; const catalogFlash = { defaultModelId: "deepseek-flash", models: [{ id: "deepseek-flash", label: "DeepSeek V4 Flash", description: "x", creditCost: 1, isDefault: true }] }; const terminalSummary = { has_resumable_case: false, has_terminal_case_with_time: true, latest_resumable: null, latest_terminal: { case_id: CASE, status: "completed" } }; const emptySummary = { has_resumable_case: false, has_terminal_case_with_time: false, latest_resumable: null, latest_terminal: null }; type Mode = { /** Paths whose response never arrives (to prove nothing waits for them). */ hang: Set; models: unknown; summary: unknown; consultStatus: unknown; accountUser: string; }; function installApi() { const originalFetch = globalThis.fetch; const requests: string[] = []; const mode: Mode = { hang: new Set(), models: catalogPro, summary: terminalSummary, consultStatus: null, accountUser: USER }; const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); globalThis.fetch = (async (input: unknown, init?: RequestInit) => { const url = new URL(String(input), "http://localhost"); const method = init?.method ?? "GET"; const path = url.pathname; requests.push(`${method} ${path}${url.search}`); if (mode.hang.has(path)) return new Promise(() => undefined); if (path === "/api/account") { return json({ user: { id: mode.accountUser, email: "fictional@example.test" }, profile: selfProfile, avatar: null, credits: 9, isAdmin: false, adminUrl: null, rectificationPriceCredits: 1, activeSubscription: null, hasConfirmedBirthTime: true, hasUsableBirthTime: true, }); } if (path === "/api/chart-profiles") return json({ profiles: [] }); if (path === "/api/models") return json(mode.models); if (path === "/api/sessions" && method === "GET") return json({ sessions: [rectRow, consultationRow], nextCursor: null }); if (path === `/api/sessions/${RECT}`) return json(method === "GET" ? { session: rectRow } : { ok: true }); if (path === `/api/sessions/${CONS}`) return json(method === "GET" ? { session: consultationRow } : { ok: true }); if (path.startsWith("/api/sessions")) return json({ ok: true }); if (path === "/api/consult/status") { if (!mode.consultStatus) return new Response(null, { status: 204 }); return json(mode.consultStatus); } if (path === "/api/rectification/cases/entry-summary") return json(mode.summary); if (path === "/api/rectification/cases/open") { return json({ disposition: "readonly", caseId: CASE, sessionId: RECT, status: "completed", shouldStartOpening: false, skillVersion: "test" }); } if (path === `/api/rectification/cases/${CASE}`) return json({ turns: [], snapshot: null }); if (path === "/api/daily-starlanguage") return new Response(null, { status: 204 }); return json({ error: "not_mocked" }, 404); }) as typeof fetch; return { requests, mode, restore: () => { globalThis.fetch = originalFetch; } }; } function memoryStorage(): Storage { const values = new Map(); return { getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, String(value)); }, removeItem: (key) => { values.delete(key); }, clear: () => values.clear(), key: (index) => Array.from(values.keys())[index] ?? null, get length() { return values.size; }, }; } function createTab() { const session = memoryStorage(); const local = memoryStorage(); const location = { origin: "http://localhost", protocol: "http:", host: "localhost", hostname: "localhost", href: "http://localhost/", pathname: "/", search: "", hash: "", assigned: [] as string[], assign(href: string) { this.assigned.push(href); }, replace(href: string) { this.assigned.push(href); }, }; const setUrl = (href: string) => { const next = new URL(href, "http://localhost"); location.pathname = next.pathname; location.search = next.search; location.href = next.href; }; const history = { pushState(_state: unknown, _title: string, url: string) { setUrl(url); }, replaceState(_state: unknown, _title: string, url: string) { setUrl(url); }, }; return { session, local, location, history, setUrl }; } type Tab = ReturnType; type FirstCommit = { loading: boolean; title: string | null; url: string; requests: number }; async function openDocument(tab: Tab, href: string, requests: string[]) { const h = createClientLifecycleHarness(); const win = globalThis.window as unknown as Record; tab.setUrl(href); Object.assign(win, { location: tab.location, history: tab.history, navigator: { onLine: true, userAgent: "node" }, sessionStorage: tab.session, localStorage: tab.local, innerWidth: 1440, Element: (globalThis as unknown as { Element: unknown }).Element, Node: (globalThis as unknown as { Element: unknown }).Element, ShadowRoot: class {}, matchMedia: (query: string) => ({ matches: false, media: query, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, }), }); const hostProto = (globalThis as unknown as { HTMLElement: { prototype: Record } }).HTMLElement.prototype; Object.assign(hostProto, { querySelector: () => null, querySelectorAll: () => [], closest: () => null, matches: () => false, getBoundingClientRect: () => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0 }), getClientRects: () => [], scrollTo() {}, scrollIntoView() {}, scrollHeight: 0, clientHeight: 0, scrollTop: 0, offsetHeight: 0, offsetTop: 0, }); if (!Object.getOwnPropertyDescriptor(hostProto, "children")) { Object.defineProperty(hostProto, "children", { configurable: true, get(this: { childNodes: unknown[] }) { return this.childNodes; } }); } const doc = globalThis.document as unknown as Record; const createElement = (doc.createElement as (tag: string) => Record).bind(doc); doc.createElement = (tag: string) => { const node = createElement(tag); node.style = { setProperty() {}, removeProperty() {}, getPropertyValue: () => "" }; return node; }; doc.getElementsByTagName = () => [doc.head]; doc.head = createElement("head"); doc.documentElement = Object.assign(createElement("html"), { dataset: {} }); doc.body = createElement("body"); const previous = { sessionStorage: Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"), localStorage: Object.getOwnPropertyDescriptor(globalThis, "localStorage"), navigator: Object.getOwnPropertyDescriptor(globalThis, "navigator"), matchMedia: Object.getOwnPropertyDescriptor(globalThis, "matchMedia"), Node: Object.getOwnPropertyDescriptor(globalThis, "Node"), requestAnimationFrame: Object.getOwnPropertyDescriptor(globalThis, "requestAnimationFrame"), cancelAnimationFrame: Object.getOwnPropertyDescriptor(globalThis, "cancelAnimationFrame"), }; Object.defineProperty(globalThis, "sessionStorage", { value: tab.session, configurable: true, writable: true }); Object.defineProperty(globalThis, "localStorage", { value: tab.local, configurable: true, writable: true }); Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true, writable: true }); Object.defineProperty(globalThis, "matchMedia", { value: win.matchMedia, configurable: true, writable: true }); Object.defineProperty(globalThis, "Node", { value: win.Node, configurable: true, writable: true }); Object.defineProperty(globalThis, "requestAnimationFrame", { value: win.requestAnimationFrame, configurable: true, writable: true }); Object.defineProperty(globalThis, "cancelAnimationFrame", { value: win.cancelAnimationFrame, configurable: true, writable: true }); // A document load re-evaluates every module: person store, warm snapshot. const subject = await import("../src/lib/current-subject.ts"); subject.resetSubjectCatalogForTests(); (await import("../src/lib/home-warm-snapshot.ts")).clearHomeWarmSnapshot(); (await import("../src/lib/home-warm-start.ts")).resetHomeWarmStartForTests(); const { SessionListProvider, useSessionList, useShellRegistration, sessionListSidebarModel } = await import("../src/lib/session-list-context.tsx"); const { SidebarInset, SidebarProvider } = await import("../src/components/ui/sidebar.tsx"); const { AppSidebar } = await import("../src/components/app-sidebar.tsx"); function AppShell({ children }: { children: ReactNode }) { const list = useSessionList(); const registration = useShellRegistration(); const { sessions, account } = sessionListSidebarModel(list, registration); return (
{children}
); } const byClass = (name: string) => h.elements().filter((node) => String(node.props.className ?? "").split(/\s+/).includes(name)); const view = () => { const title = byClass("chat-header-title")[0]?.childNodes.find((node) => node.tagName === "STRONG")?.text ?? null; const textarea = h.elements().find((node) => node.tagName === "TEXTAREA"); return { url: `${tab.location.pathname}${tab.location.search}`, loading: byClass("app-loading").length > 0 || byClass("app-loading-orbit").length > 0, title, inputDisabled: Boolean(textarea?.props.disabled), rectificationSurface: byClass("is-rectification").length > 0, starter: byClass("composer-wrap-starter").length > 0, transcript: h.elements().some((node) => node.text === "虚构回答"), rectificationCard: (byClass("starter-entry").find((node) => /再次校正|开始新的生时校正/.test(String(node.props["aria-label"] ?? "")))?.props["aria-label"] as string | undefined) ?? null, stopVisible: h.elements().some((node) => node.tagName === "BUTTON" && String(node.props["aria-label"] ?? "").includes("停止回答")), modelLabel: h.elements().find((node) => node.tagName === "BUTTON" && /DeepSeek V4/.test(node.text))?.text ?? null, }; }; // Records the first committed frame of each Home mount: a layout effect of // a sibling mounted after Home runs once Home's host tree is committed and // before the browser would paint. const firstCommits: FirstCommit[] = []; function FirstCommitProbe() { useLayoutEffect(() => { const v = view(); firstCommits.push({ loading: v.loading, title: v.title, url: v.url, requests: requests.length }); }, []); return null; } const Home = (await import("../src/app/(app)/page.tsx")).default; const route = (pathname: string): ReactNode => { if (pathname === "/") return ; return
; }; // Next's order, not the test's convenience: the router renders the next // page while the address bar still shows the page being left, and writes // the URL in an insertion effect of that commit (app-router HistoryUpdater). let routePath = tab.location.pathname; let pendingHref: string | null = null; function HistoryUpdater() { useInsertionEffect(() => { if (!pendingHref) return; tab.history.pushState(null, "", pendingHref); pendingHref = null; }); return null; } const renderRoute = () => h.render( {route(routePath)}, ); await renderRoute(); const link = (label: string) => { // Session rows carry their date after the title; match on the title. const found = h.elements().find((node) => node.tagName === "A" && (node.text.trim() === label || node.props["aria-label"] === label)) ?? h.elements().find((node) => node.tagName === "A" && String(node.props.href ?? "").startsWith("/?c=") && node.text.startsWith(label)); assert.ok(found, `link ${label} is rendered`); return found; }; async function followLink(label: string) { const anchor = link(label); const href = String(anchor.props.href); const own = anchor.props.onClick as ((event: unknown) => void) | undefined; const click = { button: 0, metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, defaultPrevented: false, preventDefault() { click.defaultPrevented = true; }, stopPropagation() {}, currentTarget: anchor, target: anchor, nativeEvent: {}, }; await h.update(() => { own?.(click); }); routePath = new URL(href, "http://localhost").pathname; pendingHref = href; await renderRoute(); return href; } async function settle(until: () => boolean, label: string, ms = 12000) { const started = Date.now(); while (Date.now() - started < ms) { await h.idle(); if (until()) return; } assert.fail(`timed out waiting for ${label}: ${JSON.stringify(view())}`); } async function close() { try { await h.close(); } finally { for (const [key, descriptor] of Object.entries(previous)) { if (descriptor) Object.defineProperty(globalThis, key, descriptor); else Reflect.deleteProperty(globalThis, key); } } } return { h, view, link, followLink, settle, close, firstCommits, errors: h.errors }; } type Doc = Awaited>; /** Cold document load of `/`, revealed and settled (the snapshot is written). */ async function coldHome(tab: Tab, api: ReturnType, href = "/"): Promise { const doc = await openDocument(tab, href, api.requests); assert.equal(doc.firstCommits[0]?.loading, true, "a document load starts on the loading ring"); const { readHomeWarmSnapshot } = await import("../src/lib/home-warm-snapshot.ts"); await doc.settle(() => doc.view().title !== null && readHomeWarmSnapshot(USER) !== null, "the cold reveal and its snapshot"); for (let i = 0; i < 5; i += 1) await doc.h.idle(); return doc; } const WAITED_PATHS = ["/api/models", "/api/consult/status", "/api/rectification/cases/entry-summary", "/api/daily-starlanguage", "/api/account", "/api/sessions", "/api/chart-profiles"]; for (const page of [ { label: "星盘", path: "/chart" }, { label: "星历", path: "/ephemeris" }, { label: "我的报告", path: "/reports" }, { label: "星盘档案", path: "/people" }, ] as const) { test(`${page.path} → 新建对话 paints a ready home in its first frame, with every request still unanswered`, async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api); assert.equal(doc.firstCommits.length, 1); await doc.followLink(page.label); assert.equal(tab.location.pathname, page.path); // From here on nothing answers. A home that waited for any of these // could never leave the loading ring. for (const path of WAITED_PATHS) api.mode.hang.add(path); const before = api.requests.length; const href = await doc.followLink("新建对话"); assert.equal(href, "/?new=1"); const first = doc.firstCommits[1]; assert.ok(first, "home mounted again"); assert.equal(first.loading, false, "no loading ring on the warm return"); assert.equal(first.title, "新对话", "the new empty chat is active in the first frame"); assert.equal(first.requests, before, "the first frame was committed before any request was sent"); const view = doc.view(); assert.equal(view.loading, false); assert.equal(view.title, "新对话"); assert.equal(view.starter, true, "starter home, not a transcript"); assert.equal(view.transcript, false); assert.equal(view.inputDisabled, false); assert.equal(view.url, "/", "?new=1 is consumed"); assert.equal(tab.session.getItem("jyotisha.session-url-return"), null); assert.equal(view.rectificationCard, "再次校正", "the rectification card uses the remembered summary"); const background = api.requests.slice(before); assert.ok(background.includes("GET /api/models"), `the catalog refreshes in the background: ${background.join(", ")}`); assert.ok(background.includes("GET /api/consult/status"), "background consultation recovery still checks"); for (let i = 0; i < 5; i += 1) await doc.h.idle(); assert.equal(doc.view().loading, false, "an unanswered background refresh never brings the ring back"); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); } test("/reports → a history row opens that conversation without the loading ring", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api, "/?new=1"); assert.equal(doc.view().title, "新对话"); await doc.followLink("我的报告"); for (const path of WAITED_PATHS) api.mode.hang.add(path); const before = api.requests.length; const href = await doc.followLink("事业方向"); assert.equal(href, `/?c=${CONS}`); const first = doc.firstCommits[1]; assert.equal(first.loading, false); assert.equal(first.title, "事业方向"); assert.equal(first.requests, before); assert.equal(doc.view().url, `/?c=${CONS}`, "?c= stays"); await doc.settle(() => doc!.view().transcript, "the conversation's messages"); assert.equal(doc.view().loading, false); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("a full reload of / still plays the loading ring once", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api); await doc.followLink("星盘"); await doc.followLink("新建对话"); assert.equal(doc.firstCommits[1].loading, false); await doc.close(); doc = undefined; // Same tab, new document: module memory is gone, storages survive. doc = await openDocument(tab, "/", api.requests); assert.equal(doc.firstCommits[0].loading, true, "reload starts on the ring"); await doc.settle(() => doc!.view().title !== null, "the cold reveal after reload"); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("a snapshot missing a piece falls back to the unchanged cold path", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api); await doc.followLink("星盘"); const { clearHomeWarmSnapshot } = await import("../src/lib/home-warm-snapshot.ts"); clearHomeWarmSnapshot(); api.mode.hang.add("/api/models"); await doc.followLink("新建对话"); assert.equal(doc.firstCommits[1].loading, true, "no snapshot → the loading ring, never a half-reveal"); for (let i = 0; i < 5; i += 1) await doc.h.idle(); assert.equal(doc.view().loading, true, "the cold path waits for the catalog as before (BUG-1021)"); assert.equal(doc.view().title, null); api.mode.hang.delete("/api/models"); // The cold path's own retry is out of scope; a new document proves it recovers. await doc.close(); doc = undefined; } finally { await doc?.close(); api.restore(); } }); test("background refresh: a model that went offline switches to the default with the existing notice", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api, `/?c=${CONS}`); assert.equal(doc.view().title, "事业方向"); await doc.followLink("星盘"); api.mode.models = catalogFlash; const { toast } = await import("sonner"); const toastsBefore = toast.getHistory().length; await doc.followLink("事业方向"); assert.equal(doc.firstCommits[1].loading, false); assert.match(doc.firstCommits[1].title ?? "", /事业方向/); await doc.settle(() => /Flash/.test(doc!.view().modelLabel ?? ""), "the default model after the catalog refresh"); const notices = toast.getHistory().slice(toastsBefore).map((item) => String((item as { title?: unknown }).title ?? "")); assert.ok(notices.includes("此前选择的模型已下线,已切换为默认模型。"), `notice: ${notices.join(" | ")}`); assert.ok(api.requests.some((line) => line === `PATCH /api/sessions/${CONS}`), "the fallback is synced like the cold path"); assert.equal(doc.view().loading, false); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("background refresh: an unfinished consultation resumes on a warm return to its conversation", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api, `/?c=${CONS}`); await doc.followLink("星盘"); // Left while an answer was still generating: the tab's pending record // survives and the server still reports the request as reserved. tab.session.setItem("jyotisha.pending-consultation", JSON.stringify({ sessionId: CONS, requestId: REQUEST, question: "虚构追问", theme: "career", entrypoint: null })); api.mode.consultStatus = { sessionId: CONS, requestId: REQUEST, status: "reserved" }; await doc.followLink("事业方向"); assert.equal(doc.firstCommits[1].loading, false); await doc.settle(() => doc!.view().stopVisible, "the recovering answer's stop control"); assert.equal(doc.view().title, "事业方向"); assert.ok(tab.session.getItem("jyotisha.pending-consultation"), "the pending record is kept while recovering"); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("background refresh: a reserved consultation keeps recovering without taking over 新建对话", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api); await doc.followLink("星盘"); tab.session.setItem("jyotisha.pending-consultation", JSON.stringify({ sessionId: CONS, requestId: REQUEST, question: "虚构追问", theme: "career", entrypoint: null })); api.mode.consultStatus = { sessionId: CONS, requestId: REQUEST, status: "reserved" }; await doc.followLink("新建对话"); await doc.settle(() => Boolean(tab.session.getItem("jyotisha.pending-consultation")) && api.requests.some((line) => line.startsWith(`GET /api/consult/status?sessionId=${CONS}`)), "the stored request to be checked"); for (let i = 0; i < 5; i += 1) await doc.h.idle(); assert.equal(doc.view().title, "新对话", "the explicit new chat stays active"); assert.equal(doc.view().starter, true); assert.ok(tab.session.getItem("jyotisha.pending-consultation"), "the older answer is still being recovered"); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("background refresh: a changed rectification entry summary replaces the card label silently", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api, "/?new=1"); assert.equal(doc.view().rectificationCard, "再次校正"); await doc.followLink("星盘"); api.mode.summary = emptySummary; await doc.followLink("新建对话"); assert.equal(doc.firstCommits[1].loading, false); await doc.settle(() => doc!.view().rectificationCard === "开始新的生时校正", "the refreshed summary"); assert.equal(doc.view().loading, false); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("a warm return to bare / after a rectification session lands on the blank starter home, not back in it", async () => { // 原值(BUG-1038/1040): 标题 "a stashed return to a rectification session reopens it on a warm return, with its URL"; // 离开时存根 === RECT → 账户页脚回裸 `/` → 地址栏写回 `/?c=RECT`、校正面重新打开、存根清空。 // 新值: 离开时不存根(null)→ 账户页脚回裸 `/` → 首帧无加载环、就是空白首页「新对话」,地址栏仍是 `/`,不打开校正面。 // 原因: BUG-1052 D1(裸 `/` 永远空白首页)+ D3(删除存根);暖路径与冷路径同规则,BUG-1040「无加载环」照旧锁住。 const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api, `/?c=${RECT}`); await doc.settle(() => doc!.view().rectificationSurface, "the rectification surface"); await doc.followLink("星盘"); assert.equal(tab.session.getItem("jyotisha.session-url-return"), null); // The read-only footer links to bare `/`. await doc.followLink("林遥,打开账户"); assert.equal(doc.firstCommits[1].loading, false); assert.equal(doc.firstCommits[1].title, "新对话", "the blank starter home is the first frame"); for (let i = 0; i < 5; i += 1) await doc.h.idle(); const view = doc.view(); assert.equal(view.url, "/"); assert.equal(view.title, "新对话"); assert.equal(view.starter, true); assert.equal(view.rectificationSurface, false); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("a cold load of bare / with an old conversation and a rectification in the list shows the blank starter home", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api); const view = doc.view(); assert.equal(view.title, "新对话"); assert.equal(view.starter, true); assert.equal(view.transcript, false); assert.equal(view.url, "/"); assert.equal(view.rectificationSurface, false); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } }); test("another account in the same tab never paints the previous account's snapshot", async () => { const tab = createTab(); const api = installApi(); let doc: Doc | undefined; try { doc = await coldHome(tab, api); const snapshot = await import("../src/lib/home-warm-snapshot.ts"); assert.ok(snapshot.readHomeWarmSnapshot(USER), "the cold reveal wrote a snapshot"); // The provider reloads (as after a person switch) and now sees another account. api.mode.accountUser = OTHER_USER; const subject = await import("../src/lib/current-subject.ts"); await doc.followLink("星盘"); await doc.h.update(() => { subject.setCurrentSubject("self"); subject.invalidateAccountFlight(); }); await doc.h.update(() => { subject.setCurrentSubject("someone-else"); }); await doc.settle(() => snapshot.readHomeWarmSnapshot(USER) === null, "the other account to clear the snapshot"); assert.equal(snapshot.readHomeWarmSnapshot(OTHER_USER), null); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); api.restore(); } });