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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea7dd95757
commit
55ebdd7a95
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-28 — 首页第一次打开少下载约 95 KB
|
||||
|
||||
- 首页第一次打开少下载约 95 KB(压缩后,约占原来的 13%):生时校正界面和回答排版组件等到用的时候才下载。
|
||||
- 进入生时校正、打开历史对话时照常一次出现,不会先空白或先出没排版的文字。
|
||||
- Skill 版本不变(BUG-1474,issue #124)。
|
||||
|
||||
## 2026-10-28 — 只知道出生时段时,咨询出字更快
|
||||
|
||||
- 只填了出生时段(没有精确到分钟)的人来咨询时,后台两次计算改成同时进行,每轮约快 0.3~1 秒。回答内容不变。
|
||||
|
||||
@@ -20111,3 +20111,18 @@
|
||||
- 关联:BUG-957(窗口咨询预计算与缓存)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/window-consult-parallel-20261028`。
|
||||
|
||||
## BUG-1474 | 空白首页首屏带着整个生时校正界面和 Markdown 渲染
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检第 14、15 条 → #124)
|
||||
- 影响面:首页 `/` 首屏下载;`frontend/src/app/(app)/page.tsx`、`hooks/use-rectification-surface.ts`、`lib/home-bootstrap-run.ts`、`components/chat-message-content.tsx`。
|
||||
- 现象:大多数人打开首页只是普通对话,却要先下载、解析整个生时校正界面(题池、精炼包、分歧面板、校正棋盘等)和 react-markdown 一整套。
|
||||
- 触发条件:任何一次冷开首页。
|
||||
- 根因:`page.tsx` 静态 import `ConversationalBirthTimeRectification`。`chat-message-content` 本来已经按需加载 Markdown,但校正界面里的 `rectification-verification-report.tsx` 静态 import 了 `chat-markdown-view`,于是 Markdown 也跟着校正界面进了首屏。
|
||||
- 修复:首页改用 `conversational-birth-time-rectification-lazy.tsx`(同名组件,代码到手后同步渲染真组件);`lib/rectification-surface-loader.ts` 负责下载。`openRectificationCase` 在发开案请求时就开始下载,并和案情读取一起等它到齐后才设置会话 / 案号,所以校正界面仍是一次揭幕、不出现空白或转圈。Markdown 的加载挪到 `lib/chat-markdown-loader.ts`(组件和启动共用);首页启动落地到有消息(或可能有消息)的会话时,揭幕前等它到齐,`?c=` 打开带消息的会话不会先出纯文本再跳格式;空白首页不等。
|
||||
- 验证:`next build` 后按首页预渲染 HTML 引用的全部 JS/CSS 做 gzip-9 合计:改前 704,165 B,改后 609,606 B(−94,559 B,−13.4%);JS 528,697 B,CSS 不变;`/` 仍 ○ Static;首屏块里不再有 micromark / mdast / remark 和校正界面模块。新测试 `frontend/tests/home-lazy-rectification-20261028.test.ts`(首页只经懒加载壳引用校正界面;开案时下载与开案请求同时开始、在设置会话之前等齐;加载器只下载一次);`home-bootstrap-run.test.ts` 新增两条(带消息的 `?c=` 会话在 Markdown 到齐前不提交;空白首页不等 Markdown;前一条在改前代码上失败)。`chat-bundle-splitting-contract.test.ts` 两处断言改认新的加载器文件;`lagna-precheck-rectify-entry.test.tsx` 两条打开测试先载入校正界面代码(tsx 现场转译超过原来 20 ms 的等待;测的仍是打开意图与会话)。全量前端测试与 staging 逐条比对无新增失败。tsc 0、lint 0 error(`page.tsx` 38 条 warning 与基线相同)。
|
||||
- 防复发:上述测试。首页首屏只放普通对话必需的代码;进入某个功能才用的界面经加载器下载,并在打开流程里等齐再挂载。
|
||||
- 关联:BUG-1130(城市表挪出首屏)、BUG-1469(#120 次级页同类问题)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/home-lazy-rectification-20261028`。
|
||||
|
||||
@@ -23,3 +23,9 @@
|
||||
11. 电脑打开首页、右侧本命盘开着,问一个会写长回答的问题:回答照常逐字出来,右侧盘不闪、不跳;出字过程中拖动右侧盘的宽度、点盘上的宫位都跟手。
|
||||
12. 回答还没出字时点「停止」:提示是「停止回答并申请退回本次点数」;出了一些字以后再问一次并点停止:提示变成「停止回答,保留已生成内容并退回本次点数」。
|
||||
|
||||
## #124 首页少下载校正界面和排版组件(BUG-1474)
|
||||
|
||||
13. 清掉浏览器缓存后打开首页:问候和输入框照常一次出来;问一个问题,回答里的加粗、列表照常有格式。
|
||||
14. 侧栏点一条以前的长对话(或直接打开带 `?c=` 的对话链接):一出现就是排好版的样子,不先闪一下纯文本。
|
||||
15. 从首页进入生时校正(说「帮我做生时纠正」,或从星盘页进入):校正界面一次出现,题目和进度都在,没有空白、没有转圈。
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
synastryReportClosed,
|
||||
rectificationOrdinaryComposerReopen,
|
||||
} from "@/lib/rectification-session-composer-guard";
|
||||
import { ConversationalBirthTimeRectification } from "@/components/conversational-birth-time-rectification";
|
||||
import { ConversationalBirthTimeRectification } from "@/components/conversational-birth-time-rectification-lazy";
|
||||
import { declaredBirthTime } from "@/lib/rectification-surface-state";
|
||||
import { toggleChatMessageFeedback } from "@/components/chat-message-actions";
|
||||
import type { ChatTranscriptActions } from "@/components/chat-transcript";
|
||||
|
||||
@@ -6,6 +6,7 @@ import { prefetchOnIdle } from "@/components/chat-chunk-prefetch";
|
||||
import { plainParagraphs } from "@/components/chat-message-paragraphs";
|
||||
import { TechniqueAuditDisclosure } from "@/components/technique-audit-disclosure";
|
||||
import { splitSpokenAndReport } from "@/lib/chat-answer-split";
|
||||
import { loadChatMarkdown, loadedChatMarkdown, type MarkdownRenderer } from "@/lib/chat-markdown-loader";
|
||||
import { splitStableMarkdown } from "@/lib/chat-markdown-split";
|
||||
import type { TechniqueAuditRow } from "@/lib/consultation-agent-events";
|
||||
import {
|
||||
@@ -13,28 +14,15 @@ import {
|
||||
splitSpokenAnswerAndTechniqueAudit,
|
||||
} from "@/lib/consultation-technique-audit";
|
||||
|
||||
type MarkdownRenderer = (text: string) => ReactNode;
|
||||
|
||||
let markdownRenderer: MarkdownRenderer | undefined;
|
||||
let markdownRendererRequest: Promise<MarkdownRenderer> | undefined;
|
||||
|
||||
function loadMarkdownRenderer() {
|
||||
markdownRendererRequest ??= import("@/components/chat-markdown-view").then((module) => {
|
||||
markdownRenderer = module.renderChatMarkdown;
|
||||
return module.renderChatMarkdown;
|
||||
});
|
||||
return markdownRendererRequest;
|
||||
}
|
||||
|
||||
prefetchOnIdle(loadMarkdownRenderer);
|
||||
prefetchOnIdle(loadChatMarkdown);
|
||||
|
||||
function useMarkdownRenderer() {
|
||||
const [renderer, setRenderer] = useState<MarkdownRenderer | undefined>(() => markdownRenderer);
|
||||
const [renderer, setRenderer] = useState<MarkdownRenderer | undefined>(() => loadedChatMarkdown());
|
||||
|
||||
useEffect(() => {
|
||||
if (renderer) return;
|
||||
let active = true;
|
||||
void loadMarkdownRenderer().then((loaded) => {
|
||||
void loadChatMarkdown().then((loaded) => {
|
||||
if (active) setRenderer(() => loaded);
|
||||
});
|
||||
return () => { active = false; };
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { ConversationalBirthTimeRectificationProps } from "@/components/conversational-birth-time-rectification";
|
||||
import { loadedRectificationSurface, loadRectificationSurface } from "@/lib/rectification-surface-loader";
|
||||
|
||||
/**
|
||||
* Home renders this in place of the surface (issue #124). Every open path
|
||||
* awaits `loadRectificationSurface` first, so the first render is already the
|
||||
* real surface; the effect only covers a chunk that failed during the open.
|
||||
*/
|
||||
export function ConversationalBirthTimeRectification(props: ConversationalBirthTimeRectificationProps) {
|
||||
const [Surface, setSurface] = useState(() => loadedRectificationSurface());
|
||||
|
||||
useEffect(() => {
|
||||
if (Surface) return;
|
||||
let active = true;
|
||||
void loadRectificationSurface().then((loaded) => {
|
||||
if (active) setSurface(() => loaded);
|
||||
}).catch(() => undefined);
|
||||
return () => { active = false; };
|
||||
}, [Surface]);
|
||||
|
||||
return Surface ? <Surface {...props} /> : null;
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
} from "@/lib/rectification-surface-state";
|
||||
import { notifyError } from "@/lib/notify";
|
||||
import { bindOfferToCase } from "@/lib/rectification-in-chat";
|
||||
import { loadRectificationSurface } from "@/lib/rectification-surface-loader";
|
||||
|
||||
export type RectificationShellState = {
|
||||
sessionId: string | null;
|
||||
@@ -231,6 +232,8 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
|
||||
rectificationOpenInFlight.current = true;
|
||||
setRectificationLoading(true);
|
||||
// The surface code downloads while the case opens (issue #124).
|
||||
const surfaceCode = loadRectificationSurface().catch(() => null);
|
||||
setRectificationError("");
|
||||
setRectificationErrorSessionId(null);
|
||||
// Read the selection source now: `selectSession` resets it synchronously
|
||||
@@ -324,6 +327,7 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
timeoutMs: RECTIFICATION_OPEN_HYDRATE_TIMEOUT_MS,
|
||||
});
|
||||
const source = await sourceRead;
|
||||
await surfaceCode;
|
||||
if (sourceSessionId) {
|
||||
// The server bound the offer card to this case; mirror it so the card
|
||||
// under that answer stops offering a new start.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type MarkdownRenderer = (text: string) => ReactNode;
|
||||
|
||||
let markdownRenderer: MarkdownRenderer | undefined;
|
||||
let markdownRendererRequest: Promise<MarkdownRenderer> | undefined;
|
||||
|
||||
/** The renderer once its chunk has arrived, for a synchronous first render. */
|
||||
export function loadedChatMarkdown(): MarkdownRenderer | undefined {
|
||||
return markdownRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* react-markdown and remark-gfm stay out of the first screen (issue #124).
|
||||
* The home bootstrap awaits this before revealing a conversation that has
|
||||
* messages, so a `?c=` landing never shows plain text first.
|
||||
*/
|
||||
export function loadChatMarkdown(): Promise<MarkdownRenderer> {
|
||||
markdownRendererRequest ??= import("@/components/chat-markdown-view").then((module) => {
|
||||
markdownRenderer = module.renderChatMarkdown;
|
||||
return module.renderChatMarkdown;
|
||||
});
|
||||
return markdownRendererRequest;
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import type { PublicLanguageModelCatalog } from "@/lib/public-models";
|
||||
import { startRectificationEntrySummaryRead } from "@/lib/rectification-entry-summary-read";
|
||||
import { takeBootRead } from "@/lib/boot-early-reads";
|
||||
import { preloadHomeOnboardingShell } from "@/lib/home-onboarding-shell-loader";
|
||||
import { loadChatMarkdown } from "@/lib/chat-markdown-loader";
|
||||
import { dropRectificationStoredPending } from "@/lib/rectification-session-composer-guard";
|
||||
import type { SessionListBoot } from "@/lib/session-list-context";
|
||||
import { isUnsavedEmptyConsultation, replaceUnsavedEmptyConsultations } from "@/lib/session-list-filter";
|
||||
@@ -111,6 +112,8 @@ export type HomeBootstrapIo = {
|
||||
clearLoginSessionReturn: typeof clearLoginSessionReturn;
|
||||
startEntrySummaryRead: () => void;
|
||||
preloadOnboardingShell: () => Promise<void>;
|
||||
/** Markdown rendering for a landing conversation that has messages (issue #124). */
|
||||
preloadChatMarkdown: () => Promise<void>;
|
||||
restoreConsultationRecovery: (
|
||||
session: ChatSession,
|
||||
requestId: string,
|
||||
@@ -163,6 +166,7 @@ const defaultIo: Omit<HomeBootstrapIo, "restoreConsultationRecovery"> = {
|
||||
clearLoginSessionReturn,
|
||||
startEntrySummaryRead: () => startRectificationEntrySummaryRead(),
|
||||
preloadOnboardingShell: preloadHomeOnboardingShell,
|
||||
preloadChatMarkdown: () => loadChatMarkdown().then(() => undefined, () => undefined),
|
||||
};
|
||||
|
||||
function resolveIo(deps: HomeBootstrapDeps): HomeBootstrapIo {
|
||||
@@ -431,6 +435,11 @@ export async function resolveLanding(
|
||||
}
|
||||
const activeListed = nextSessions.find((session) => session.id === landingSessionId)
|
||||
?? nextSessions[0];
|
||||
// Markdown is not in the first screen (issue #124): a conversation that has
|
||||
// or may have messages reveals already formatted, not plain text first.
|
||||
const markdownReady = activeListed && (activeListed.messages.length > 0 || !activeListed.messagesHydrated)
|
||||
? io.preloadChatMarkdown()
|
||||
: null;
|
||||
if (activeListed && !activeListed.messagesHydrated && activeListed.sessionType === "consultation") {
|
||||
try {
|
||||
const detailed = await io.fetchSessionDetail(activeListed.id, ready.nextModelCatalog, signal);
|
||||
@@ -442,6 +451,7 @@ export async function resolveLanding(
|
||||
if (caught instanceof LoginRedirectError) return;
|
||||
}
|
||||
}
|
||||
if (markdownReady && nextSessions.find((session) => session.id === landingSessionId)?.messages.length) await markdownReady;
|
||||
if (signal.aborted) return;
|
||||
io.clearStaleClientReload(deps.storage);
|
||||
deps.commit.setSessionsCursor(ready.cursor);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import type { ConversationalBirthTimeRectificationProps } from "@/components/conversational-birth-time-rectification";
|
||||
|
||||
type Surface = ComponentType<ConversationalBirthTimeRectificationProps>;
|
||||
|
||||
let surface: Surface | undefined;
|
||||
let surfaceRequest: Promise<Surface> | undefined;
|
||||
|
||||
/** The surface once its chunk has arrived, for a synchronous first render. */
|
||||
export function loadedRectificationSurface(): Surface | undefined {
|
||||
return surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rectification surface (and the markdown it reaches) is not in the home
|
||||
* first screen (issue #124). Opening a case awaits this alongside the case
|
||||
* hydration, so the surface still mounts once, already drawn.
|
||||
*/
|
||||
export function loadRectificationSurface(): Promise<Surface> {
|
||||
surfaceRequest ??= import("@/components/conversational-birth-time-rectification").then((module) => {
|
||||
surface = module.ConversationalBirthTimeRectification;
|
||||
return surface;
|
||||
}).catch((error: unknown) => {
|
||||
// A failed chunk may succeed on the next open.
|
||||
surfaceRequest = undefined;
|
||||
throw error;
|
||||
});
|
||||
return surfaceRequest;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const markdownViewSource = readProjectFile("src/components/chat-markdown-view.ts
|
||||
const messageRowSource = readProjectFile("src/components/chat-message-row.tsx");
|
||||
const activitySource = readProjectFile("src/components/agent-activity-status.tsx");
|
||||
const prefetchSource = readProjectFile("src/components/chat-chunk-prefetch.ts");
|
||||
const markdownLoaderSource = readProjectFile("src/lib/chat-markdown-loader.ts");
|
||||
|
||||
test("the markdown pipeline is split out of the initial chat bundle", () => {
|
||||
// Given: the chat surface statically imports the message content component.
|
||||
@@ -17,8 +18,12 @@ test("the markdown pipeline is split out of the initial chat bundle", () => {
|
||||
assert.doesNotMatch(contentSource, /^import .*"remark-gfm"/m);
|
||||
|
||||
// Then: the renderer only arrives through a dynamic import.
|
||||
assert.match(contentSource, /import\("@\/components\/chat-markdown-view"\)/);
|
||||
assert.match(contentSource, /markdownRenderer = module\.renderChatMarkdown/);
|
||||
// 原值:chat-message-content.tsx 里直接 import("@/components/chat-markdown-view")
|
||||
// 新值:动态 import 挪到 lib/chat-markdown-loader.ts,组件和首页启动共用这一份
|
||||
// 原因:issue #124 首页启动要在揭幕前等 Markdown(`?c=` 打开带消息的会话)
|
||||
assert.doesNotMatch(contentSource, /^import .*"@\/components\/chat-markdown-view"/m);
|
||||
assert.match(markdownLoaderSource, /import\("@\/components\/chat-markdown-view"\)/);
|
||||
assert.match(markdownLoaderSource, /markdownRenderer = module\.renderChatMarkdown/);
|
||||
assert.match(markdownViewSource, /^import ReactMarkdown, \{ type Components \} from "react-markdown";$/m);
|
||||
assert.match(markdownViewSource, /^import remarkGfm from "remark-gfm";$/m);
|
||||
assert.match(markdownViewSource, /remarkPlugins=\{\[remarkGfm\]\}/);
|
||||
@@ -105,7 +110,8 @@ test("deferred chat chunks are requested during idle time after first paint", ()
|
||||
assert.match(prefetchSource, /if \(typeof window === "undefined"\) return;/);
|
||||
assert.match(prefetchSource, /window\.requestIdleCallback\(request, \{ timeout: 2_000 \}\)/);
|
||||
assert.match(prefetchSource, /window\.setTimeout\(request, 300\)/);
|
||||
assert.match(contentSource, /prefetchOnIdle\(loadMarkdownRenderer\)/);
|
||||
// 原值:prefetchOnIdle(loadMarkdownRenderer);新值与原因同上(issue #124)
|
||||
assert.match(contentSource, /prefetchOnIdle\(loadChatMarkdown\)/);
|
||||
// 原值: prefetchOnIdle(loadGsap) / 新值: prefetchOnIdle(loadMessageMotion)
|
||||
// 原因: 同上,空闲预取的是 Motion 而不是 GSAP。
|
||||
assert.match(messageRowSource, /if \(motionPreferred\(\)\) prefetchOnIdle\(loadMessageMotion\)/);
|
||||
|
||||
@@ -135,6 +135,9 @@ function createHarness(options: {
|
||||
// 原因: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");
|
||||
@@ -204,6 +207,50 @@ test("a successful bootstrap fills the home once, after every read has settled",
|
||||
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)],
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Issue #124: the rectification surface and react-markdown are not in the
|
||||
* home first screen. Opening a case still mounts the surface once, drawn.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
const page = read("../src/app/(app)/page.tsx");
|
||||
const surfaceHook = read("../src/hooks/use-rectification-surface.ts");
|
||||
const messageContent = read("../src/components/chat-message-content.tsx");
|
||||
|
||||
test("home imports the surface through the lazy wrapper only", () => {
|
||||
assert.match(page, /from "@\/components\/conversational-birth-time-rectification-lazy"/);
|
||||
assert.doesNotMatch(page, /from "@\/components\/conversational-birth-time-rectification"/);
|
||||
// Nothing else the home reaches statically pulls in react-markdown.
|
||||
assert.doesNotMatch(messageContent, /from "@\/components\/chat-markdown-view"/);
|
||||
assert.match(messageContent, /loadChatMarkdown/);
|
||||
});
|
||||
|
||||
test("opening a case awaits the surface code before the ids that mount it are set", () => {
|
||||
const started = surfaceHook.indexOf("const surfaceCode = loadRectificationSurface()");
|
||||
const awaited = surfaceHook.indexOf("await surfaceCode;");
|
||||
const mounted = surfaceHook.indexOf("setRectificationSessionId(opened.sessionId);");
|
||||
const opened = surfaceHook.indexOf('await fetch("/api/rectification/cases/open"');
|
||||
assert.ok(started > 0 && started < opened, "the download starts with the open request");
|
||||
assert.ok(awaited > opened && awaited < mounted, "the surface mounts only after its code is in");
|
||||
});
|
||||
|
||||
test("the wrapper renders the real surface synchronously once it is loaded", async () => {
|
||||
const { loadRectificationSurface, loadedRectificationSurface } = await import("../src/lib/rectification-surface-loader.ts");
|
||||
assert.equal(loadedRectificationSurface(), undefined);
|
||||
const loaded = await loadRectificationSurface();
|
||||
assert.equal(loadedRectificationSurface(), loaded);
|
||||
assert.equal(await loadRectificationSurface(), loaded, "one download per page");
|
||||
const surfaceModule = await import("../src/components/conversational-birth-time-rectification.tsx");
|
||||
assert.equal(loaded, surfaceModule.ConversationalBirthTimeRectification);
|
||||
});
|
||||
|
||||
test("the markdown loader hands out one renderer", async () => {
|
||||
const { loadChatMarkdown, loadedChatMarkdown } = await import("../src/lib/chat-markdown-loader.ts");
|
||||
const renderer = await loadChatMarkdown();
|
||||
assert.equal(loadedChatMarkdown(), renderer);
|
||||
assert.equal(typeof renderer, "function");
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import type { Account, Profile } from "../src/lib/home-types.ts";
|
||||
import type { PublicLanguageModelCatalog } from "../src/lib/public-models.ts";
|
||||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
||||
import { loadRectificationSurface } from "../src/lib/rectification-surface-loader.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
@@ -127,6 +128,11 @@ async function openFromChart(disposition: "resumed" | "created") {
|
||||
const posts: Array<{ intent?: string }> = [];
|
||||
const previousFetch = globalThis.fetch;
|
||||
const harness = createClientLifecycleHarness();
|
||||
// 原值:打开流程不等校正界面代码,两次 idle(约 20 ms)就设好会话
|
||||
// 新值:先把校正界面代码载入,再走同样的打开流程
|
||||
// 原因:issue #124 校正界面改为按需下载,打开时要等它到齐才设会话(一次揭幕);
|
||||
// tsx 现场转译 74 个文件远超 20 ms,本组测的是打开意图与会话,不是下载时长
|
||||
await loadRectificationSurface();
|
||||
installLocation("?rectify=1");
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
|
||||
Reference in New Issue
Block a user