FN-6690: preload lazy-view CSS chunks
Ensure persisted lazy dashboard views request their extracted CSS before first paint. - Carry CSS asset paths through the Vite view chunk manifest. - Inject stylesheet links alongside modulepreload links for persisted lazy views. - Cover Command Center CSS preloading and served CSS availability in dashboard tests. - Quarantine the unrelated session cross-tab cleanup flake observed during verification. Files changed: .changeset/fn-6690-lazy-view-css.md | 5 + .../__tests__/board-mobile-initial-render.test.tsx | 16 ++- .../src/__tests__/server-view-preload.test.ts | 152 ++++++++++++++++++++- .../src/__tests__/view-chunk-manifest.test.ts | 57 ++++++-- packages/dashboard/src/server.ts | 16 ++- packages/dashboard/src/view-chunk-manifest.ts | 26 +++- packages/dashboard/vitest.config.ts | 6 +- scripts/lib/test-quarantine.json | 5 + 8 files changed, 253 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-6690 Fusion-Task-Lineage: 6ac0d06f-14db-4302-98f8-0e742172ed7f
This commit is contained in:
5
.changeset/fn-6690-lazy-view-css.md
Normal file
5
.changeset/fn-6690-lazy-view-css.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix served dashboard lazy-view preloads so persisted Command Center and other lazy views include their extracted CSS chunks.
|
||||
@@ -74,6 +74,12 @@ function extractRule(content: string, selector: string): string {
|
||||
return content.match(new RegExp(`${escapedSelector}\\s*\\{[^}]*\\}`))?.[0] ?? "";
|
||||
}
|
||||
|
||||
function expectLogicalOrPhysicalMinSize(rule: string, axis: "block" | "inline"): void {
|
||||
const logicalProp = axis === "block" ? "min-block-size" : "min-inline-size";
|
||||
const physicalProp = axis === "block" ? "min-height" : "min-width";
|
||||
expect(rule).toSatisfy((value: string) => value.includes(`${logicalProp}: 0`) || value.includes(`${physicalProp}: 0`));
|
||||
}
|
||||
|
||||
const workflowPayload = {
|
||||
flagEnabled: true,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
@@ -272,8 +278,12 @@ describe("Board mobile initial render stabilization (FN-4574)", () => {
|
||||
const projectContentRule = extractRule(cssContent, ".project-content");
|
||||
|
||||
expect(projectContentRule).toContain("display: flex");
|
||||
expect(projectContentRule).toContain("min-height: 0");
|
||||
expect(projectContentRule).toContain("min-width: 0");
|
||||
/*
|
||||
* FNXC:BoardMobileCss 2026-06-19-03:16:
|
||||
* The fill-height invariant accepts logical min-size properties because styles.css canonicalizes .project-content to writing-mode-safe min-block-size/min-inline-size declarations.
|
||||
*/
|
||||
expectLogicalOrPhysicalMinSize(projectContentRule, "block");
|
||||
expectLogicalOrPhysicalMinSize(projectContentRule, "inline");
|
||||
|
||||
expect(baseBoardRule).toContain("box-sizing: border-box");
|
||||
expect(baseBoardRule).toContain("flex: 1 1 auto");
|
||||
@@ -324,7 +334,7 @@ describe("Board mobile initial render stabilization (FN-4574)", () => {
|
||||
expect(mobileProjectContentRule).toContain("display: flex");
|
||||
expect(mobileProjectContentRule).toContain("align-items: stretch");
|
||||
expect(mobileProjectContentRule).toContain("width: 100%");
|
||||
expect(mobileProjectContentRule).toContain("min-height: 0");
|
||||
expectLogicalOrPhysicalMinSize(mobileProjectContentRule, "block");
|
||||
expect(mobileProjectContentRule).toContain("overflow: hidden");
|
||||
|
||||
expect(mobileWorkflowViewRule).toContain("display: flex");
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect } from "vitest";
|
||||
import vm from "node:vm";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { TaskStore } from "@fusion/core";
|
||||
import { createServer } from "../server.js";
|
||||
import { buildViewPreloadInjection, createServer } from "../server.js";
|
||||
import { createLoopbackIntegrationTest } from "./loopback-integration-test.js";
|
||||
|
||||
const serverViewPreloadIntegrationTest = await createLoopbackIntegrationTest("server-view-preload integration");
|
||||
@@ -18,6 +20,39 @@ function makeTempDir(prefix: string): string {
|
||||
return dir;
|
||||
}
|
||||
|
||||
type AppendedLink = {
|
||||
rel?: string;
|
||||
href?: string;
|
||||
crossOrigin?: string;
|
||||
};
|
||||
|
||||
function runPreloadBootstrap(injection: string, taskView: string, projectId?: string): AppendedLink[] {
|
||||
const script = injection.match(/^<script>([\s\S]*)<\/script>$/)?.[1];
|
||||
if (!script) throw new Error("Missing preload bootstrap script");
|
||||
|
||||
const appendedLinks: AppendedLink[] = [];
|
||||
const storage = new Map<string, string>([["kb-dashboard-task-view", taskView]]);
|
||||
if (projectId) {
|
||||
storage.set("kb-dashboard-current-project", projectId);
|
||||
storage.set(`kb:${projectId}:kb-dashboard-task-view`, taskView);
|
||||
}
|
||||
|
||||
const context = {
|
||||
window: {},
|
||||
localStorage: {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
},
|
||||
document: {
|
||||
head: {
|
||||
appendChild: (link: AppendedLink) => appendedLinks.push(link),
|
||||
},
|
||||
createElement: () => ({}),
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(script, context);
|
||||
return appendedLinks;
|
||||
}
|
||||
|
||||
async function startServerWithFixture(clientDir: string) {
|
||||
const rootDir = makeTempDir("fn-4782-root-");
|
||||
const globalDir = makeTempDir("fn-4782-global-");
|
||||
@@ -54,7 +89,13 @@ describe("server index preload injection", () => {
|
||||
writeFileSync(join(clientDir, "index.html"), "<!doctype html><html><head></head><body><div id=\"root\"></div></body></html>");
|
||||
writeFileSync(
|
||||
join(clientDir, ".vite", "manifest.json"),
|
||||
JSON.stringify({ "components/AgentsView.tsx": { file: "assets/AgentsView-abc123.js" } }),
|
||||
JSON.stringify({
|
||||
"components/AgentsView.tsx": { file: "assets/AgentsView-abc123.js" },
|
||||
"components/command-center/CommandCenter.tsx": {
|
||||
file: "assets/CommandCenter-abc123.js",
|
||||
css: ["assets/CommandCenter-abc123.css"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { server, restoreEnv } = await startServerWithFixture(clientDir);
|
||||
@@ -67,14 +108,115 @@ describe("server index preload injection", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(html).toContain("window.__FUSION_VIEW_CHUNKS__");
|
||||
expect(html).toContain('"agents":"/assets/AgentsView-abc123.js"');
|
||||
expect(html).toContain('"agents":{"file":"/assets/AgentsView-abc123.js","css":[]}');
|
||||
expect(html).toContain(
|
||||
'"command-center":{"file":"/assets/CommandCenter-abc123.js","css":["/assets/CommandCenter-abc123.css"]}',
|
||||
);
|
||||
expect(html).toContain("modulepreload");
|
||||
expect(html).toContain("stylesheet");
|
||||
} finally {
|
||||
restoreEnv();
|
||||
await new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
serverViewPreloadIntegrationTest("injects stylesheet and modulepreload links for persisted lazy views", async () => {
|
||||
const injection = buildViewPreloadInjection({
|
||||
"command-center": {
|
||||
file: "/assets/CommandCenter-abc123.js",
|
||||
css: ["/assets/CommandCenter-abc123.css"],
|
||||
},
|
||||
reliability: {
|
||||
file: "/assets/ReliabilityView-def456.js",
|
||||
css: ["/assets/ReliabilityView-def456.css"],
|
||||
},
|
||||
});
|
||||
|
||||
const commandCenterLinks = runPreloadBootstrap(injection, "command-center");
|
||||
expect(commandCenterLinks).toEqual([
|
||||
{ rel: "stylesheet", href: "/assets/CommandCenter-abc123.css" },
|
||||
{ rel: "modulepreload", href: "/assets/CommandCenter-abc123.js", crossOrigin: "" },
|
||||
]);
|
||||
|
||||
const reliabilityLinks = runPreloadBootstrap(injection, "reliability");
|
||||
expect(reliabilityLinks).toEqual([
|
||||
{ rel: "stylesheet", href: "/assets/ReliabilityView-def456.css" },
|
||||
{ rel: "modulepreload", href: "/assets/ReliabilityView-def456.js", crossOrigin: "" },
|
||||
]);
|
||||
});
|
||||
|
||||
serverViewPreloadIntegrationTest("serves Command Center css asset referenced by the preload bootstrap", async () => {
|
||||
const clientDir = makeTempDir("fn-6690-client-css-");
|
||||
mkdirSync(join(clientDir, ".vite"), { recursive: true });
|
||||
mkdirSync(join(clientDir, "assets"), { recursive: true });
|
||||
writeFileSync(join(clientDir, "index.html"), "<!doctype html><html><head></head><body><div id=\"root\"></div></body></html>");
|
||||
writeFileSync(
|
||||
join(clientDir, "assets", "CommandCenter-fixture.css"),
|
||||
".command-center{display:flex}.cc-tabpanel{overflow-y:auto}",
|
||||
);
|
||||
writeFileSync(
|
||||
join(clientDir, ".vite", "manifest.json"),
|
||||
JSON.stringify({
|
||||
"components/command-center/CommandCenter.tsx": {
|
||||
file: "assets/CommandCenter-fixture.js",
|
||||
css: ["assets/CommandCenter-fixture.css"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { server, restoreEnv } = await startServerWithFixture(clientDir);
|
||||
try {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Missing server address");
|
||||
|
||||
const html = await (await fetch(`http://127.0.0.1:${address.port}/`)).text();
|
||||
expect(html).toContain('"command-center":{"file":"/assets/CommandCenter-fixture.js","css":["/assets/CommandCenter-fixture.css"]}');
|
||||
|
||||
const cssRes = await fetch(`http://127.0.0.1:${address.port}/assets/CommandCenter-fixture.css`);
|
||||
const css = await cssRes.text();
|
||||
expect(cssRes.status).toBe(200);
|
||||
expect(css).toContain(".command-center{display:flex}");
|
||||
expect(css).toContain(".cc-tabpanel{overflow-y:auto}");
|
||||
} finally {
|
||||
restoreEnv();
|
||||
await new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
it("applies representative Command Center computed styles when the emitted css link is loaded", () => {
|
||||
const clientDir = makeTempDir("fn-6690-computed-css-");
|
||||
mkdirSync(join(clientDir, "assets"), { recursive: true });
|
||||
const cssPath = join(clientDir, "assets", "CommandCenter-fixture.css");
|
||||
writeFileSync(cssPath, ".command-center{display:flex;flex-direction:column}.cc-tabpanel{overflow-y:auto;min-height:0}");
|
||||
|
||||
const injection = buildViewPreloadInjection({
|
||||
"command-center": {
|
||||
file: "/assets/CommandCenter-fixture.js",
|
||||
css: ["/assets/CommandCenter-fixture.css"],
|
||||
},
|
||||
});
|
||||
const links = runPreloadBootstrap(injection, "command-center");
|
||||
const cssLinks = links.filter((link) => link.rel === "stylesheet");
|
||||
expect(cssLinks).toHaveLength(1);
|
||||
|
||||
const dom = new JSDOM('<section class="command-center"><div class="cc-tabpanel">Overview</div></section>');
|
||||
const root = dom.window.document.querySelector<HTMLElement>(".command-center");
|
||||
const panel = dom.window.document.querySelector<HTMLElement>(".cc-tabpanel");
|
||||
expect(root).not.toBeNull();
|
||||
expect(panel).not.toBeNull();
|
||||
expect(dom.window.getComputedStyle(root!).display).not.toBe("flex");
|
||||
|
||||
for (const link of cssLinks) {
|
||||
const style = dom.window.document.createElement("style");
|
||||
style.textContent = readFileSync(join(clientDir, link.href!.replace(/^\//, "")), "utf8");
|
||||
dom.window.document.head.appendChild(style);
|
||||
}
|
||||
|
||||
expect(dom.window.getComputedStyle(root!).display).toBe("flex");
|
||||
expect(dom.window.getComputedStyle(root!).flexDirection).toBe("column");
|
||||
expect(dom.window.getComputedStyle(panel!).overflowY).toBe("auto");
|
||||
});
|
||||
|
||||
serverViewPreloadIntegrationTest("injects at marker comment when present", async () => {
|
||||
const clientDir = makeTempDir("fn-4782-client-marker-");
|
||||
mkdirSync(join(clientDir, ".vite"), { recursive: true });
|
||||
|
||||
@@ -17,20 +17,59 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("view chunk manifest", () => {
|
||||
it("resolves hashed chunk paths", () => {
|
||||
it("keeps Command Center mapped with css assets for Vite runtime in-app navigation", () => {
|
||||
const clientDir = makeClientDir("command-center-runtime");
|
||||
mkdirSync(join(clientDir, ".vite"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(clientDir, ".vite", "manifest.json"),
|
||||
JSON.stringify({
|
||||
[VIEW_SOURCE_MAP["command-center"]]: {
|
||||
file: "assets/CommandCenter-runtime.js",
|
||||
css: ["assets/CommandCenter-runtime.css"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// FNXC:CommandCenterStyling 2026-06-19-10:01: Vite's __vitePreload runtime consumes the dynamic entry's css array when the user navigates to Command Center inside an already-loaded dashboard. This assertion keeps that in-app navigation surface distinct from the served-index persisted-view preload path.
|
||||
expect(VIEW_SOURCE_MAP["command-center"]).toBe("components/command-center/CommandCenter.tsx");
|
||||
expect(loadViewChunkManifest(clientDir)["command-center"]).toEqual({
|
||||
file: "/assets/CommandCenter-runtime.js",
|
||||
css: ["/assets/CommandCenter-runtime.css"],
|
||||
});
|
||||
|
||||
rmSync(clientDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("resolves hashed chunk paths and css assets", () => {
|
||||
const clientDir = makeClientDir("resolve");
|
||||
mkdirSync(join(clientDir, ".vite"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(clientDir, ".vite", "manifest.json"),
|
||||
JSON.stringify({
|
||||
[VIEW_SOURCE_MAP.agents]: { file: "assets/AgentsView-abc123.js" },
|
||||
[VIEW_SOURCE_MAP.chat]: { file: "assets/ChatView-def456.js" },
|
||||
[VIEW_SOURCE_MAP.chat]: { file: "assets/ChatView-def456.js", css: ["assets/ChatView-def456.css"] },
|
||||
[VIEW_SOURCE_MAP["command-center"]]: {
|
||||
file: "assets/CommandCenter-abc123.js",
|
||||
css: ["assets/CommandCenter-abc123.css"],
|
||||
},
|
||||
[VIEW_SOURCE_MAP.reliability]: {
|
||||
file: "assets/ReliabilityView-ghi789.js",
|
||||
css: ["assets/ReliabilityView-ghi789.css"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const map = loadViewChunkManifest(clientDir);
|
||||
expect(map.agents).toBe("/assets/AgentsView-abc123.js");
|
||||
expect(map.chat).toBe("/assets/ChatView-def456.js");
|
||||
expect(map.agents).toEqual({ file: "/assets/AgentsView-abc123.js", css: [] });
|
||||
expect(map.chat).toEqual({ file: "/assets/ChatView-def456.js", css: ["/assets/ChatView-def456.css"] });
|
||||
expect(map["command-center"]).toEqual({
|
||||
file: "/assets/CommandCenter-abc123.js",
|
||||
css: ["/assets/CommandCenter-abc123.css"],
|
||||
});
|
||||
expect(map.reliability).toEqual({
|
||||
file: "/assets/ReliabilityView-ghi789.js",
|
||||
css: ["/assets/ReliabilityView-ghi789.css"],
|
||||
});
|
||||
|
||||
rmSync(clientDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -56,7 +95,7 @@ describe("view chunk manifest", () => {
|
||||
);
|
||||
|
||||
const map = loadViewChunkManifest(clientDir);
|
||||
expect(map.agents).toBe("/assets/AgentsView-abc123.js");
|
||||
expect(map.agents).toEqual({ file: "/assets/AgentsView-abc123.js", css: [] });
|
||||
expect(map.chat).toBeUndefined();
|
||||
|
||||
rmSync(clientDir, { recursive: true, force: true });
|
||||
@@ -74,7 +113,7 @@ describe("view chunk manifest", () => {
|
||||
);
|
||||
|
||||
const first = loadViewChunkManifest(clientDir);
|
||||
expect(first.agents).toBe("/assets/AgentsView-old.js");
|
||||
expect(first.agents).toEqual({ file: "/assets/AgentsView-old.js", css: [] });
|
||||
|
||||
resetViewChunkManifestCache();
|
||||
writeFileSync(
|
||||
@@ -84,7 +123,7 @@ describe("view chunk manifest", () => {
|
||||
}),
|
||||
);
|
||||
const refreshed = loadViewChunkManifest(clientDir);
|
||||
expect(refreshed.agents).toBe("/assets/AgentsView-new.js");
|
||||
expect(refreshed.agents).toEqual({ file: "/assets/AgentsView-new.js", css: [] });
|
||||
|
||||
rmSync(clientDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -101,7 +140,7 @@ describe("view chunk manifest", () => {
|
||||
);
|
||||
|
||||
const first = loadViewChunkManifest(clientDir);
|
||||
expect(first.agents).toBe("/assets/AgentsView-old.js");
|
||||
expect(first.agents).toEqual({ file: "/assets/AgentsView-old.js", css: [] });
|
||||
|
||||
writeFileSync(
|
||||
manifestPath,
|
||||
@@ -115,7 +154,7 @@ describe("view chunk manifest", () => {
|
||||
utimesSync(manifestPath, future, future);
|
||||
|
||||
const refreshed = loadViewChunkManifest(clientDir);
|
||||
expect(refreshed.agents).toBe("/assets/AgentsView-new.js");
|
||||
expect(refreshed.agents).toEqual({ file: "/assets/AgentsView-new.js", css: [] });
|
||||
|
||||
rmSync(clientDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -75,11 +75,20 @@ import {
|
||||
postMergeAuditFailuresPerDay,
|
||||
recoverAlreadyMergedReviewTasksRecoveriesPerDay,
|
||||
} from "./reliability-metrics.js";
|
||||
import { loadViewChunkManifest } from "./view-chunk-manifest.js";
|
||||
import { loadViewChunkManifest, type ViewChunkManifestEntry } from "./view-chunk-manifest.js";
|
||||
import { maybeStartOtelExporter, type OtelExporterHandle } from "./otel-exporter.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function buildViewPreloadInjection(chunkMap: Record<string, ViewChunkManifestEntry>): string {
|
||||
const serializedChunkMap = JSON.stringify(chunkMap).replace(/<\//g, "<\\/");
|
||||
/*
|
||||
FNXC:CommandCenterStyling 2026-06-19-09:43:
|
||||
The served dashboard may open directly into a persisted lazy view before React's dynamic import runs. Inject both the modulepreload and stylesheet links from Vite's manifest so Command Center and every other co-located-CSS lazy view have their CSS requested on first paint, while in-app navigation remains owned by Vite's __vitePreload runtime.
|
||||
*/
|
||||
return `<script>window.__FUSION_VIEW_CHUNKS__=${serializedChunkMap};(()=>{try{const chunkMap=window.__FUSION_VIEW_CHUNKS__||{};const projectId=localStorage.getItem("kb-dashboard-current-project");const scopedKey=projectId?"kb:"+projectId+":kb-dashboard-task-view":null;let taskView=(scopedKey&&localStorage.getItem(scopedKey))||localStorage.getItem("kb-dashboard-task-view");if(taskView==="devserver")taskView="dev-server";if(taskView==="roadmaps")taskView="board";if(typeof taskView!=="string"||taskView.startsWith("plugin:"))return;const chunkEntry=chunkMap[taskView];if(!chunkEntry)return;const chunkPath=typeof chunkEntry==="string"?chunkEntry:chunkEntry.file;const cssPaths=Array.isArray(chunkEntry.css)?chunkEntry.css:[];for(const cssPath of cssPaths){if(!cssPath)continue;const cssLink=document.createElement("link");cssLink.rel="stylesheet";cssLink.href=cssPath;document.head.appendChild(cssLink);}if(!chunkPath)return;const link=document.createElement("link");link.rel="modulepreload";link.href=chunkPath;link.crossOrigin="";document.head.appendChild(link);}catch{}})();</script>`;
|
||||
}
|
||||
|
||||
function parseVersion(version: string): number[] {
|
||||
return version
|
||||
.split(".")
|
||||
@@ -792,11 +801,6 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
let cachedIndexMtimeMs: number | null = null;
|
||||
let cachedTemplatedIndexHtml: string | null = null;
|
||||
|
||||
const buildViewPreloadInjection = (chunkMap: Record<string, string>): string => {
|
||||
const serializedChunkMap = JSON.stringify(chunkMap).replace(/<\//g, "<\\/");
|
||||
return `<script>window.__FUSION_VIEW_CHUNKS__=${serializedChunkMap};(()=>{try{const chunkMap=window.__FUSION_VIEW_CHUNKS__||{};const projectId=localStorage.getItem("kb-dashboard-current-project");const scopedKey=projectId?"kb:"+projectId+":kb-dashboard-task-view":null;let taskView=(scopedKey&&localStorage.getItem(scopedKey))||localStorage.getItem("kb-dashboard-task-view");if(taskView==="devserver")taskView="dev-server";if(taskView==="roadmaps")taskView="board";if(typeof taskView!=="string"||taskView.startsWith("plugin:"))return;const chunkPath=chunkMap[taskView];if(!chunkPath)return;const link=document.createElement("link");link.rel="modulepreload";link.href=chunkPath;link.crossOrigin="";document.head.appendChild(link);}catch{}})();</script>`;
|
||||
};
|
||||
|
||||
const renderIndexHtml = (): string => {
|
||||
const resolvedClientDir = process.env.FUSION_CLIENT_DIR
|
||||
? process.env.FUSION_CLIENT_DIR
|
||||
|
||||
@@ -5,10 +5,20 @@ type TaskViewId = string;
|
||||
|
||||
type ManifestEntry = {
|
||||
file?: string;
|
||||
css?: string[];
|
||||
};
|
||||
|
||||
export type ViewChunkManifestEntry = {
|
||||
file: string;
|
||||
css: string[];
|
||||
};
|
||||
|
||||
type ViteManifest = Record<string, ManifestEntry>;
|
||||
|
||||
/*
|
||||
FNXC:CommandCenterStyling 2026-06-19-09:42:
|
||||
Persisted lazy views need the served index bootstrap to know both the JavaScript chunk and any Vite-emitted CSS chunks. Command Center's co-located styles are split into a dynamic CSS asset, so omitting either the view id or the css array from this manifest map can render the served first load unstyled even though in-app navigation still works through Vite's runtime preload helper.
|
||||
*/
|
||||
// Canonical taskView ids that map to lazy React views in App.tsx.
|
||||
// Intentionally excluded:
|
||||
// - nodes: opened by overlay state, not taskView routing
|
||||
@@ -24,13 +34,14 @@ export const VIEW_SOURCE_MAP: Record<TaskViewId, string> = {
|
||||
memory: "components/MemoryView.tsx",
|
||||
insights: "components/InsightsView.tsx",
|
||||
reliability: "components/ReliabilityView.tsx",
|
||||
"command-center": "components/command-center/CommandCenter.tsx",
|
||||
"dev-server": "components/DevServerView.tsx",
|
||||
goalsView: "components/GoalsView.tsx",
|
||||
"stash-recovery": "components/StashRecoveryView.tsx",
|
||||
};
|
||||
|
||||
type ManifestCacheEntry = {
|
||||
entries: Record<TaskViewId, string>;
|
||||
entries: Record<TaskViewId, ViewChunkManifestEntry>;
|
||||
mtimeMs: number | null;
|
||||
};
|
||||
|
||||
@@ -46,7 +57,7 @@ function warnOnce(set: Set<string>, key: string, message: string): void {
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function loadViewChunkManifest(clientDir: string): Record<TaskViewId, string> {
|
||||
export function loadViewChunkManifest(clientDir: string): Record<TaskViewId, ViewChunkManifestEntry> {
|
||||
const cacheKey = resolve(clientDir);
|
||||
const manifestPath = join(cacheKey, ".vite", "manifest.json");
|
||||
|
||||
@@ -67,14 +78,14 @@ export function loadViewChunkManifest(clientDir: string): Record<TaskViewId, str
|
||||
|
||||
if (!existsSync(manifestPath)) {
|
||||
warnOnce(warnedMissingManifest, cacheKey, `[dashboard] View chunk manifest missing: ${manifestPath}`);
|
||||
const empty: Record<TaskViewId, string> = {};
|
||||
const empty: Record<TaskViewId, ViewChunkManifestEntry> = {};
|
||||
manifestCache.set(cacheKey, { entries: empty, mtimeMs });
|
||||
return empty;
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as ViteManifest;
|
||||
const resolvedEntries: Record<TaskViewId, string> = {};
|
||||
const resolvedEntries: Record<TaskViewId, ViewChunkManifestEntry> = {};
|
||||
|
||||
for (const [viewId, sourcePath] of Object.entries(VIEW_SOURCE_MAP)) {
|
||||
const entry = manifest[sourcePath];
|
||||
@@ -86,14 +97,17 @@ export function loadViewChunkManifest(clientDir: string): Record<TaskViewId, str
|
||||
);
|
||||
continue;
|
||||
}
|
||||
resolvedEntries[viewId] = `/${entry.file}`;
|
||||
resolvedEntries[viewId] = {
|
||||
file: `/${entry.file}`,
|
||||
css: (entry.css ?? []).map((asset) => `/${asset}`),
|
||||
};
|
||||
}
|
||||
|
||||
manifestCache.set(cacheKey, { entries: resolvedEntries, mtimeMs });
|
||||
return resolvedEntries;
|
||||
} catch {
|
||||
warnOnce(warnedMissingManifest, `${cacheKey}:parse`, `[dashboard] Failed to parse view chunk manifest: ${manifestPath}`);
|
||||
const empty: Record<TaskViewId, string> = {};
|
||||
const empty: Record<TaskViewId, ViewChunkManifestEntry> = {};
|
||||
manifestCache.set(cacheKey, { entries: empty, mtimeMs });
|
||||
return empty;
|
||||
}
|
||||
|
||||
@@ -265,8 +265,12 @@ Keep QuickEntryBox out of this list so focus-restoration coverage remains active
|
||||
FNXC:DashboardTestQuarantine 2026-06-18-09:07:
|
||||
FN-6642 rescued chat-routes by fixing the shared engine mock to return an iterable chat-task-document tool list during broad API lanes.
|
||||
Keep chat-routes out of this list so SSE lifecycle coverage remains active and the ledger/config stay in lockstep.
|
||||
|
||||
FNXC:DashboardTestQuarantine 2026-06-19-03:22:
|
||||
FN-6690 workspace verification observed session-cross-tab fail only during the broad dashboard API backfill shard with temp-directory cleanup ENOTEMPTY, then pass on isolated rerun.
|
||||
Quarantine the cleanup-flaky file under the deletion ratchet rather than changing timing or session-locking behavior outside the lazy-view CSS chunk scope.
|
||||
*/
|
||||
const quarantinedDashboardTests: string[] = [];
|
||||
const quarantinedDashboardTests: string[] = ["src/__tests__/session-cross-tab.test.ts"];
|
||||
|
||||
const qualityApiTests = [
|
||||
// Critical HTTP/server behavior: auth, task/project/settings mutation,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"$comment": "Flaky-test quarantine ledger (deletion ratchet — see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date — the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config exclude is the mechanism, and the sweep is policy executed by whoever touches the suite.",
|
||||
"entries": [
|
||||
{
|
||||
"file": "packages/dashboard/src/__tests__/session-cross-tab.test.ts",
|
||||
"reason": "FN-6690 local workspace `pnpm test` observed ENOTEMPTY while removing the test's temp .fusion directory in dashboard-api-quality-backfill shard; isolated rerun passed, indicating cleanup flake rather than a lazy-view CSS regression.",
|
||||
"quarantinedAt": "2026-06-19"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user