feat(FN-4671): complete Step 5-7 — add startup chain tests and changeset

Fusion-Task-Id: FN-4671
Fusion-Task-Lineage: d6a084c7-eafd-4775-8284-8dd6d9eeb842
This commit is contained in:
Fusion
2026-05-15 15:00:35 -07:00
committed by gsxdsm
parent c66de36fbd
commit caf32fc55b
5 changed files with 110 additions and 8 deletions

View File

@@ -0,0 +1,25 @@
export const DASHBOARD_STARTUP_STATUS = {
initializingTaskStore: "Initializing task store…",
startingFileWatcher: "Starting file watcher…",
initializingAgentStore: "Initializing agent store…",
startingAgents: "Starting agents…",
startingEngine: "Starting engine…",
} as const;
export type DashboardTuiStartupLike = {
start: () => Promise<void>;
setLoadingStatus: (status: string) => void;
};
export async function defaultEventLoopYield(): Promise<void> {
await new Promise<void>((resolve) => setImmediate(resolve));
}
export async function runTuiStartupPrelude(
tui: DashboardTuiStartupLike,
yieldFn: () => Promise<void> = defaultEventLoopYield,
): Promise<void> {
await tui.start();
await yieldFn();
tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.initializingTaskStore);
}

View File

@@ -210,6 +210,16 @@ describe("DashboardApp smoke", () => {
unmount();
});
it("renders ready duration when startup has completed", () => {
const controller = newController();
const { lastFrame, unmount, rerender } = render(renderDashboardAppNode(controller));
controller.setSystemInfo({ ...makeSystemInfo(), startupDurationMs: 1234 });
controller.setReady(true);
rerender(renderDashboardAppNode(controller));
expect(lastFrame() ?? "").toContain("Ready in 1.2s");
unmount();
});
it("shows interactive empty-state when no data source is wired", () => {
const controller = newController();
controller.setSystemInfo(makeSystemInfo());

View File

@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from "vitest";
import { DashboardTUI } from "../controller.js";
import {
DASHBOARD_STARTUP_STATUS,
runTuiStartupPrelude,
} from "../../dashboard-startup-chain.js";
describe("dashboard startup chain", () => {
it("exposes isReady and startupDurationMs on snapshot", () => {
const controller = new DashboardTUI();
controller.setSystemInfo({
host: "localhost",
port: 4040,
baseUrl: "http://localhost:4040",
authEnabled: false,
engineMode: "active",
fileWatcher: true,
startTimeMs: Date.now(),
startupDurationMs: 1234,
});
controller.setReady(true);
const snapshot = controller.getSnapshot();
expect(snapshot.isReady).toBe(true);
expect(snapshot.systemInfo?.startupDurationMs).toBe(1234);
});
// Keep this helper-focused so we can validate startup ordering without
// importing runDashboard() and all of its heavy runtime dependencies.
it("yields once after start before first loading status", async () => {
const calls: string[] = [];
const tui = {
start: vi.fn(async () => {
calls.push("start");
}),
setLoadingStatus: vi.fn((status: string) => {
calls.push(`status:${status}`);
}),
};
const yieldFn = vi.fn(async () => {
calls.push("yield");
});
await runTuiStartupPrelude(tui, yieldFn);
expect(calls).toEqual([
"start",
"yield",
`status:${DASHBOARD_STARTUP_STATUS.initializingTaskStore}`,
]);
expect(yieldFn).toHaveBeenCalledTimes(1);
});
it("exports the five startup status labels in order", () => {
expect(Object.values(DASHBOARD_STARTUP_STATUS)).toEqual([
"Initializing task store…",
"Starting file watcher…",
"Initializing agent store…",
"Starting agents…",
"Starting engine…",
]);
});
});

View File

@@ -64,6 +64,7 @@ import { ensureBundledDependencyGraphPluginInstalled, ensureBundledPluginInstall
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
import { syncStartupModels } from "./startup-model-sync.js";
import { DashboardTUI, DashboardLogSink, isTTYAvailable, type SystemInfo, type GitStatus, type GitCommit, type GitCommitDetail, type GitBranch, type GitWorktree, type FileEntry, type FileReadResult, type TaskStep as TUITaskStep, type TaskLogEntry as TUITaskLogEntry, type TaskDetailData, type TaskEvent } from "./dashboard-tui/index.js";
import { DASHBOARD_STARTUP_STATUS, runTuiStartupPrelude } from "./dashboard-startup-chain.js";
// Re-export for backward compatibility with tests
export { promptForPort };
@@ -816,10 +817,8 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
await store.getGlobalSettingsStore().updateSettings(patch);
},
});
// Start the TUI
await tui.start();
await new Promise<void>((resolve) => setImmediate(resolve));
tui.setLoadingStatus("Initializing task store…");
// Start the TUI and yield once so Ink can paint before init work.
await runTuiStartupPrelude(tui);
// Wire the TUI into the log sink so all console output routes through TUI
logSink.setTUI(tui);
@@ -835,7 +834,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
store = new TaskStore(cwd);
await store.init();
if (tui) tui.setLoadingStatus("Starting file watcher…");
if (tui) tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.startingFileWatcher);
await store.watch();
// Set up database health check for diagnostics
@@ -1034,10 +1033,10 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
// and are properly managed throughout their lifecycle (creation, state
// transitions, termination). Passed to TaskExecutor for agent spawning.
//
if (tui) tui.setLoadingStatus("Initializing agent store…");
if (tui) tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.initializingAgentStore);
agentStore = new AgentStore({ rootDir: store.getFusionDir() });
await agentStore.init();
if (tui) tui.setLoadingStatus("Starting agents…");
if (tui) tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.startingAgents);
// ── Reactive TUI Updates ─────────────────────────────────────────────
//
@@ -1468,7 +1467,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
let localNodeIdForMesh: string | undefined;
// Start the AI engine (unless in dev mode)
if (tui) tui.setLoadingStatus("Starting engine…");
if (tui) tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.startingEngine);
if (!opts.dev) {
// ── ProjectEngineManager: uniform engine lifecycle for all projects ──
//