perf(frontend): split chat streaming, load Inter, isolate admin CSS
Independent Staging Quality Gate / validate (push) Successful in 10m14s
Independent Staging Quality Gate / publish (push) Successful in 11m5s

Settled messages no longer rebuild on every token, Inter is actually
requested, and admin routes drop the 33 KB chat stylesheet. Root
force-dynamic is gone so public shells can prerender without changing
the no-store Cache-Control contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-29 03:39:43 +08:00
co-authored by Cursor
parent bf697d71a2
commit 3198fb6b2b
41 changed files with 1528 additions and 307 deletions
@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { createElement, createRef } from "react";
import { renderToString } from "react-dom/server";
import test from "node:test";
import {
AccountDialogOverlay,
type AccountOverlayModel,
} from "../src/components/account-dialog-overlay.tsx";
test("a closed account overlay does not render profile or logout content", () => {
const overlayRef = createRef<HTMLElement | null>() as AccountOverlayModel["overlayRef"];
const closeButtonRef = createRef<HTMLButtonElement | null>() as AccountOverlayModel["closeButtonRef"];
const modelRef = createRef<AccountOverlayModel | null>() as { current: AccountOverlayModel | null };
let profileRenders = 0;
let logoutRenders = 0;
modelRef.current = {
title: "个人资料",
dialogClass: "profile-modal",
signingOut: false,
close() {},
takeOpenLibraryRequest() { return false; },
overlayRef,
closeButtonRef,
renderProfile() {
profileRenders += 1;
return null;
},
renderLogout() {
logoutRenders += 1;
return null;
},
};
const html = renderToString(createElement(AccountDialogOverlay, {
open: false,
dialog: null,
openEpoch: 0,
modelRef,
}));
assert.equal(html, "");
assert.equal(profileRenders, 0);
assert.equal(logoutRenders, 0);
});
@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
chartLibraryFromCloudOthers,
chartLibrarySessionBranch,
keepLocalChartLibraryOnCloudFailure,
} from "../src/lib/chart-library-session.ts";
type RecordShape = { id: string; role: "self" | "other" };
function upsertSelf(library: RecordShape[], profile: unknown): RecordShape[] {
const others = library.filter((record) => record.role !== "self");
if (!profile) return others;
return [{ id: "self", role: "self" }, ...others];
}
test("clearing the account empties the chart library session", () => {
assert.equal(chartLibrarySessionBranch(null, "acct-1"), "clear");
assert.equal(chartLibrarySessionBranch("", "acct-1"), "clear");
});
test("switching accounts hydrates local then cloud instead of writing the previous library", () => {
assert.equal(chartLibrarySessionBranch("acct-2", "acct-1"), "hydrate-then-persist");
});
test("a later profile change on the same account still upserts self", () => {
assert.equal(chartLibrarySessionBranch("acct-1", "acct-1"), "persist-self");
const current: RecordShape[] = [{ id: "other-1", role: "other" }];
assert.deepEqual(upsertSelf(current, { name: "self" }), [
{ id: "self", role: "self" },
{ id: "other-1", role: "other" },
]);
});
test("a successful cloud read keeps only non-self records before upserting self", () => {
const cloud: RecordShape[] = [
{ id: "stale-self", role: "self" },
{ id: "other-1", role: "other" },
];
assert.deepEqual(chartLibraryFromCloudOthers(cloud, { name: "self" }, upsertSelf), [
{ id: "self", role: "self" },
{ id: "other-1", role: "other" },
]);
});
test("a failed cloud read leaves the local library in place", () => {
const local: RecordShape[] = [{ id: "self", role: "self" }, { id: "other-1", role: "other" }];
assert.equal(keepLocalChartLibraryOnCloudFailure(local), local);
});
@@ -97,7 +97,7 @@ test("the streaming reply announces state transitions, never streamed deltas", (
// And: the old start-only region that lived inside the busy message list is gone.
assert.doesNotMatch(pageSource, /<span className="sr-only" aria-live="polite">\{isLoading \? "Jyotisha 正在回答" : ""\}<\/span>/);
assert.match(pageSource, /<div className="message-list" aria-busy=\{isLoading\}>\n\s*\{chatMessageViews\(/);
assert.match(pageSource, /<div className="message-list" aria-busy=\{isLoading\}>\n\s*<ChatTranscript/);
});
test("the reply phase covers start, completion and every terminal state", () => {
+2 -1
View File
@@ -5,6 +5,7 @@ import test from "node:test";
import { chatMessageViews } from "../src/lib/chat-message-view.ts";
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const transcriptSource = readFileSync(new URL("../src/components/chat-transcript.tsx", import.meta.url), "utf8");
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const messageRowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
const activitySource = readFileSync(new URL("../src/components/agent-activity-status.tsx", import.meta.url), "utf8");
@@ -182,7 +183,7 @@ test("ordinary consultation replies reuse the shared Agent action bar", () => {
for (const label of ["赞", "踩", "复制回答", "重新生成回答"]) {
assert.match(actionsSource, new RegExp(`aria-label="${label}"`));
}
assert.match(pageSource, /<ChatMessageActions/);
assert.match(transcriptSource, /<ChatMessageActions/);
assert.match(pageSource, /toggleChatMessageFeedback/);
assert.match(pageSource, /function regenerateLatestAnswer\(renderKey: string\)/);
assert.match(pageSource, /messages: session\.messages\.slice\(0, -1\)/);
@@ -233,14 +233,15 @@ test("homepage creation and sidebar selection resolve through distinct server in
test("an answered conversation offers no suggested follow-up questions", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const transcript = readFileSync(new URL("../src/components/chat-transcript.tsx", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
// Given: the chips above the composer stay gone. Grounded continuations sit under
// the latest answer and send immediately instead of filling the composer.
assert.doesNotMatch(source, /composer-suggestions|activeSuggestions|chooseConversationSuggestion/);
assert.doesNotMatch(styles, /composer-suggestions/);
assert.match(source, /ConversationFollowUps/);
assert.match(source, /deriveConsultationFollowUps/);
assert.match(transcript, /ConversationFollowUps/);
assert.match(transcript, /deriveConsultationFollowUps/);
assert.match(styles, /\.conversation-follow-ups/);
// Then: nothing in the conversation carries or stores a suggestion list any more.
@@ -0,0 +1,77 @@
import assert from "node:assert/strict";
import { createElement, createRef } from "react";
import { renderToString } from "react-dom/server";
import test from "node:test";
import {
SettledMessageList,
StreamingMessageEntry,
UnsplitChatTranscript,
type ChatTranscriptActions,
type ChatTranscriptProps,
} from "../src/components/chat-transcript.tsx";
import type { ChatMessage } from "../src/lib/chat-message-view.ts";
import { streamingChatMessageView } from "../src/lib/chat-message-view.ts";
import {
disableHomeStreamingRenderProbe,
enableHomeStreamingRenderProbe,
homeStreamingRenderProbeSnapshot,
resetHomeStreamingRenderProbe,
} from "../src/lib/home-streaming-render-probe.ts";
function propsFor(messages: readonly ChatMessage[]): Omit<ChatTranscriptProps, "loading" | "streamingText"> {
const actionsRef = createRef<ChatTranscriptActions>() as ChatTranscriptProps["actionsRef"];
actionsRef.current = {
onFeedback() {},
onCopy() {},
onRegenerate() {},
onFollowUp() {},
};
return {
messages,
sessionId: "session-test",
sessionType: "consultation",
theme: "career",
messageFeedback: {},
copiedMessageKey: null,
cancellationPending: false,
productEntrypointsDisabled: true,
actionsRef,
};
}
test("the split architecture renders settled history once while streaming tokens", () => {
const messages: ChatMessage[] = [
{ role: "user", text: "问题一" },
{ role: "assistant", text: "回答一,需要足够长才能生成追问卡片。" },
{ role: "user", text: "问题二" },
{ role: "assistant", text: "回答二,需要足够长才能生成追问卡片。" },
{ role: "user", text: "请继续说明这个月的安排。" },
];
const tokens = ["甲", "甲乙", "甲乙丙", "甲乙丙丁", "甲乙丙丁戊"];
const base = propsFor(messages);
resetHomeStreamingRenderProbe();
enableHomeStreamingRenderProbe();
renderToString(createElement(SettledMessageList, { ...base, loading: true }));
for (const streamingText of tokens) {
const streamingMessage = streamingChatMessageView(messages, true, streamingText);
assert.ok(streamingMessage);
renderToString(createElement(StreamingMessageEntry, { message: streamingMessage }));
}
const split = homeStreamingRenderProbeSnapshot();
disableHomeStreamingRenderProbe();
resetHomeStreamingRenderProbe();
enableHomeStreamingRenderProbe();
for (const streamingText of tokens) {
renderToString(createElement(UnsplitChatTranscript, { ...base, loading: true, streamingText }));
}
const unsplit = homeStreamingRenderProbeSnapshot();
disableHomeStreamingRenderProbe();
assert.equal(split.settledListRenders, 1);
assert.equal(split.streamingRowRenders, tokens.length);
assert.equal(unsplit.unsplitListRenders, tokens.length);
assert.ok(unsplit.settledRowRenders > split.settledRowRenders);
});
@@ -63,9 +63,9 @@ const RELOCATION_2015_PROBE = {
question: "2015 年 5 月前后有没有搬家或长期住到外地?",
candidate_ids: ["05:00", "05:10"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no" as const, supports: ["05:10"], conflicts: ["05:00"] },
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
],
information_gain: 0.87,
source: "dasha_boundary",
@@ -80,9 +80,9 @@ const EDUCATION_2014_PROBE = {
question: "2014 年前后有没有升学、转学或换学习环境?",
candidate_ids: ["05:00", "05:10"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no" as const, supports: ["05:10"], conflicts: ["05:00"] },
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
],
information_gain: 0.64,
source: "dasha_activation",
@@ -118,8 +118,8 @@ const YEARLESS_D24_PROBE = {
question: "引擎给出的区分机会绑定 D24。",
candidate_ids: ["05:00", "05:10"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no" as const, supports: ["05:10"], conflicts: ["05:00"] },
],
information_gain: 2.5,
source: "varga_contrast",
@@ -558,7 +558,7 @@ test("clicking A applies the choice without invoking a language model", async ()
test("answering a discriminator persists the next dated card so GET still has a tap target", async () => {
const closed = parseV9CaseDossier(twoProbeDossier());
assert.ok(closed);
closed.conversationSummary.activeFocus = null;
Object.assign(closed.conversationSummary, { activeFocus: null });
assert.equal(choiceCardFromCaseDossier(closed), null);
const accounting = persistChoiceAccounting(twoProbeDossier(), {
@@ -608,7 +608,7 @@ test("answering a discriminator persists the next dated card so GET still has a
const refreshed = parseV9CaseDossier(twoProbeDossier());
assert.ok(refreshed);
const answered = twoProbeInference();
refreshed.latestResult = {
Object.assign(refreshed, { latestResult: {
...refreshed.latestResult!,
decisionReceipt: {
...(refreshed.latestResult?.decisionReceipt ?? {}),
@@ -623,8 +623,8 @@ test("answering a discriminator persists the next dated card so GET still has a
}],
},
},
};
refreshed.conversationSummary.activeFocus = {
} });
Object.assign(refreshed.conversationSummary, { activeFocus: {
id: NEXT_FOCUS_ID,
caseId: CASE_ID,
questionId: String(setFocus.args.p_question_id),
@@ -636,7 +636,7 @@ test("answering a discriminator persists the next dated card so GET still has a
status: "active",
askedAt: "2026-08-28T07:37:50.000Z",
resolvedAt: null,
};
} });
const card = choiceCardFromCaseDossier(refreshed);
assert.ok(card);
assert.equal(card.focus_id, NEXT_FOCUS_ID);
@@ -10,6 +10,7 @@ import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-n
import { selectDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { contrastPacketFromDossier, decideFromDossier, overlayPublicDecision } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { buildMethodFollowupPlan, conversationalSessionOutcome } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { projectTurnDecision } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
import { parseV9CaseDossier, parseV9ComputeProjection } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { safeCaseProjection } from "../src/mastra/rectification-v9-tools.ts";
@@ -398,24 +399,24 @@ test("career and relationship training still discriminates before family or occu
const familyProbe = {
year: 2013,
year_label: "2013 年 3 月前后",
domain: "family",
domain: "family" as const,
event_family: "家人结婚、添丁或住院",
source: "dasha_boundary",
tracks: ["vimshottari", "narayana"],
source: "dasha_boundary" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false,
unique_minute_claim: false as const,
user_meaning: "时间范围锁定 2013 年 3 月前后。",
role: "distinguish",
phase: "candidate_discriminator",
role: "distinguish" as const,
phase: "candidate_discriminator" as const,
information_gain: 1.2,
semantic_key: "family.2013.03.dasha_boundary",
candidate_split_hash: "family.2013.03",
candidate_ids: ["05:00", "05:07", "05:12"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07", "05:12"] },
{ answer_class: "weak_yes", supports: ["05:07"], conflicts: ["05:00", "05:12"] },
{ answer_class: "no", supports: ["05:12"], conflicts: ["05:00", "05:07"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:07", "05:12"] },
{ answer_class: "weak_yes" as const, supports: ["05:07"], conflicts: ["05:00", "05:12"] },
{ answer_class: "no" as const, supports: ["05:12"], conflicts: ["05:00", "05:07"] },
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
],
style_options: [
{ label: "明确发生且时间吻合", answer_class: "yes" },
@@ -423,7 +424,7 @@ test("career and relationship training still discriminates before family or occu
{ label: "明确没有发生", answer_class: "no" },
{ label: "这段记不清楚", answer_class: "unsure" },
],
};
} satisfies DiscriminatingEventProbe;
const dossier = {
evidence,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
const siteStyles = readFileSync(new URL("../src/app/site-styles.ts", import.meta.url), "utf8");
const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8");
const adminCss = readFileSync(new URL("../src/app/admin/admin.css", import.meta.url), "utf8");
const homePage = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const loginPage = readFileSync(new URL("../src/app/login/page.tsx", import.meta.url), "utf8");
test("Inter is loaded through next/font and applied as a CSS variable", () => {
assert.match(layout, /import \{ Inter \} from "next\/font\/google"/);
assert.match(layout, /display: "swap"/);
assert.match(layout, /variable: "--font-inter"/);
assert.match(layout, /className=\{inter\.variable\}/);
assert.match(readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"), /var\(--font-inter, Inter\)/);
assert.match(
readFileSync(new URL("../src/components/admin/admin-app.tsx", import.meta.url), "utf8"),
/var\(--font-inter, Inter\)/,
);
});
test("site chrome owns globals.css; admin does not import it", () => {
assert.match(siteStyles, /import "\.\/globals\.css"/);
assert.match(siteStyles, /import "\.\/birth-time-choice\.css"/);
assert.doesNotMatch(layout, /globals\.css|birth-time-choice\.css|site-styles/);
assert.match(homePage, /import "@\/app\/site-styles"/);
assert.match(loginPage, /import "\.\.\/site-styles"/);
assert.doesNotMatch(adminLayout, /globals\.css|site-styles/);
assert.doesNotMatch(
readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8"),
/site-styles|globals\.css/,
);
assert.doesNotMatch(
readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8"),
/site-styles|globals\.css/,
);
assert.match(adminCss, /--font-body: StyreneB, var\(--font-inter, Inter\)/);
});
+10 -1
View File
@@ -67,7 +67,16 @@ test("the web build stamps the Git SHA as Next deploymentId and does not cache t
assert.match(nextConfig, /source: "\/"/);
assert.match(nextConfig, /source: "\/login"/);
assert.match(nextConfig, /Cache-Control["'],\s*value: "private, no-store, must-revalidate"/);
assert.match(layoutSource, /export const dynamic = "force-dynamic"/);
// Root force-dynamic was locking every route into ƒ Dynamic, including shells
// that never read cookies(). Chat HTML stays uncached via the headers above;
// routes that actually need a request stay dynamic on their own modules.
assert.doesNotMatch(layoutSource, /export const dynamic = "force-dynamic"/);
const loginPage = readFileSync(new URL("../src/app/login/page.tsx", import.meta.url), "utf8");
const reportsPage = readFileSync(new URL("../src/app/reports/page.tsx", import.meta.url), "utf8");
const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8");
assert.match(loginPage, /export const dynamic = "force-dynamic"/);
assert.match(reportsPage, /export const dynamic = "force-dynamic"/);
assert.match(adminLayout, /export const dynamic = "force-dynamic"/);
assert.match(layoutSource, /StaleClientRecovery/);
});