fix(FN-2098): align no-task heartbeat guidance and stabilize flaky tests

- Update HEARTBEAT_NO_TASK_SYSTEM_PROMPT copy to emphasize inbox, memory, delegation, and heartbeat_done usage
- Expand heartbeat monitor tests to assert no-task prompt/tool alignment and preserve task-scoped prompt behavior
- Harden first-run and App view tests by using a safe cwd fallback and more robust async UI waits
- Add best-effort dashboard performance reporting hooks in App and useProjects via a new reportDashboardPerf API helper
This commit is contained in:
Fusion
2026-04-19 01:30:33 -07:00
committed by gsxdsm
parent d9a806f61a
commit ca6fefd6ca
7 changed files with 115 additions and 23 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useMemo } from "react";
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import type { Task, TaskDetail } from "@fusion/core";
import { Header, useViewportMode } from "./components/Header";
import { Board } from "./components/Board";
@@ -49,7 +49,7 @@ import { useRemoteNodeData } from "./hooks/useRemoteNodeData";
import { useRemoteNodeEvents } from "./hooks/useRemoteNodeEvents";
import { NodeProvider, useNodeContext } from "./context/NodeContext";
import type { AiSessionSummary } from "./api";
import { fetchAiSession, fetchUnreadCount } from "./api";
import { fetchAiSession, fetchUnreadCount, reportDashboardPerf } from "./api";
function AppInner() {
const { toasts, addToast, removeToast } = useToast();
@@ -133,6 +133,9 @@ function AppInner() {
);
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const mountTimeRef = useRef(performance.now());
const projectsReadyLoggedRef = useRef(false);
const projectReadyLoggedRef = useRef(false);
const loadingStage = useMemo<DashboardLoaderStage>(() => {
if (projectsLoading) return "projects";
@@ -140,6 +143,21 @@ function AppInner() {
return "tasks";
}, [projectsLoading, currentProjectLoading]);
useEffect(() => {
if (!projectsLoading && !projectsReadyLoggedRef.current) {
projectsReadyLoggedRef.current = true;
const msg = `projects loaded at ${Math.round(performance.now() - mountTimeRef.current)}ms from mount`;
console.log(`[App] ${msg}`);
reportDashboardPerf("[App]", msg);
}
if (!currentProjectLoading && !projectReadyLoggedRef.current) {
projectReadyLoggedRef.current = true;
const msg = `current-project resolved at ${Math.round(performance.now() - mountTimeRef.current)}ms from mount`;
console.log(`[App] ${msg}`);
reportDashboardPerf("[App]", msg);
}
}, [projectsLoading, currentProjectLoading]);
useEffect(() => {
if (initialLoadComplete) {
return;
@@ -149,7 +167,11 @@ function AppInner() {
return;
}
const settleStart = performance.now();
const settleTimer = window.setTimeout(() => {
const msg = `dashboard ready at ${Math.round(performance.now() - mountTimeRef.current)}ms from mount (settle delay=${Math.round(performance.now() - settleStart)}ms)`;
console.log(`[App] ${msg}`);
reportDashboardPerf("[App]", msg);
setInitialLoadComplete(true);
}, 200);

View File

@@ -3454,6 +3454,19 @@ export function fetchProjectsAcrossNodes(): Promise<ProjectInfoWithSource[]> {
return api<ProjectInfoWithSource[]>("/projects/across-nodes");
}
/**
* Append a client-side perf measurement to the shared dashboard-perf log on disk.
* Used when browser devtools aren't available (e.g. mobile). Best-effort.
*/
export function reportDashboardPerf(source: string, message: string): void {
void api("/_perf/dashboard-load", {
method: "POST",
body: JSON.stringify({ source, message }),
}).catch(() => {
// best-effort only
});
}
/** Fetch all registered nodes */
export function fetchNodes(): Promise<NodeInfo[]> {
return api<NodeInfo[]>("/nodes");

View File

@@ -1205,18 +1205,15 @@ describe("App view switching", () => {
it("renders AgentsView when agents view is selected", async () => {
render(<App />);
// Wait for the header to render
await waitFor(() => {
expect(screen.getByTitle("Agents view")).toBeTruthy();
});
const agentsViewButton = await screen.findByTitle("Agents view", {}, { timeout: 5000 });
// Click to switch to agents view
fireEvent.click(screen.getByTitle("Agents view"));
fireEvent.click(agentsViewButton);
// Agents view should be rendered (it has a agents-view container)
await waitFor(() => {
expect(document.querySelector(".agents-view")).toBeTruthy();
});
}, { timeout: 5000 });
// Should NOT show board or list view
expect(document.querySelector(".board")).toBeNull();
@@ -1228,15 +1225,13 @@ describe("App view switching", () => {
render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Agents view")).toBeTruthy();
});
const agentsViewButton = await screen.findByTitle("Agents view", {}, { timeout: 5000 });
fireEvent.click(screen.getByTitle("Agents view"));
fireEvent.click(agentsViewButton);
await waitFor(() => {
expect(localStorage.getItem(taskViewStorageKey())).toBe("agents");
});
}, { timeout: 5000 });
});
it("initializes agents view from localStorage if saved", async () => {

View File

@@ -3,6 +3,7 @@ import type { ProjectInfo } from "../api";
import {
fetchProjectsAcrossNodes,
registerProject,
reportDashboardPerf,
unregisterProject,
updateProject,
type ProjectCreateInput,
@@ -59,13 +60,22 @@ export function useProjects(): UseProjectsResult {
async function load() {
setLoading(true);
const t0 = performance.now();
try {
const data = await fetchProjectsAcrossNodes();
const elapsed = Math.round(performance.now() - t0);
const msg = `initial fetchProjectsAcrossNodes took ${elapsed}ms (${data.length} projects)`;
console.log(`[useProjects] ${msg}`);
reportDashboardPerf("[useProjects]", msg);
if (!cancelled) {
setProjects(data);
setError(null);
}
} catch (err) {
const elapsed = Math.round(performance.now() - t0);
const msg = `initial fetch failed after ${elapsed}ms: ${err instanceof Error ? err.message : String(err)}`;
console.warn(`[useProjects] ${msg}`);
reportDashboardPerf("[useProjects]", msg);
if (!cancelled) {
setError(err instanceof Error ? err.message : "Failed to fetch projects");
}