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:
25
packages/cli/src/commands/dashboard-startup-chain.ts
Normal file
25
packages/cli/src/commands/dashboard-startup-chain.ts
Normal 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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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…",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 ──
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user