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:
5
.changeset/fn-4671-tui-yield.md
Normal file
5
.changeset/fn-4671-tui-yield.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Dashboard TUI now yields after startup so the splash paints before blocking init runs, and shows a "Ready in Xs" startup-duration indicator once initialization completes.
|
||||||
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();
|
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", () => {
|
it("shows interactive empty-state when no data source is wired", () => {
|
||||||
const controller = newController();
|
const controller = newController();
|
||||||
controller.setSystemInfo(makeSystemInfo());
|
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 { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
||||||
import { syncStartupModels } from "./startup-model-sync.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 { 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
|
// Re-export for backward compatibility with tests
|
||||||
export { promptForPort };
|
export { promptForPort };
|
||||||
@@ -816,10 +817,8 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
await store.getGlobalSettingsStore().updateSettings(patch);
|
await store.getGlobalSettingsStore().updateSettings(patch);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Start the TUI
|
// Start the TUI and yield once so Ink can paint before init work.
|
||||||
await tui.start();
|
await runTuiStartupPrelude(tui);
|
||||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
||||||
tui.setLoadingStatus("Initializing task store…");
|
|
||||||
|
|
||||||
// Wire the TUI into the log sink so all console output routes through TUI
|
// Wire the TUI into the log sink so all console output routes through TUI
|
||||||
logSink.setTUI(tui);
|
logSink.setTUI(tui);
|
||||||
@@ -835,7 +834,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
|
|
||||||
store = new TaskStore(cwd);
|
store = new TaskStore(cwd);
|
||||||
await store.init();
|
await store.init();
|
||||||
if (tui) tui.setLoadingStatus("Starting file watcher…");
|
if (tui) tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.startingFileWatcher);
|
||||||
await store.watch();
|
await store.watch();
|
||||||
|
|
||||||
// Set up database health check for diagnostics
|
// 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
|
// and are properly managed throughout their lifecycle (creation, state
|
||||||
// transitions, termination). Passed to TaskExecutor for agent spawning.
|
// 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() });
|
agentStore = new AgentStore({ rootDir: store.getFusionDir() });
|
||||||
await agentStore.init();
|
await agentStore.init();
|
||||||
if (tui) tui.setLoadingStatus("Starting agents…");
|
if (tui) tui.setLoadingStatus(DASHBOARD_STARTUP_STATUS.startingAgents);
|
||||||
|
|
||||||
// ── Reactive TUI Updates ─────────────────────────────────────────────
|
// ── Reactive TUI Updates ─────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
@@ -1468,7 +1467,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
let localNodeIdForMesh: string | undefined;
|
let localNodeIdForMesh: string | undefined;
|
||||||
|
|
||||||
// Start the AI engine (unless in dev mode)
|
// 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) {
|
if (!opts.dev) {
|
||||||
// ── ProjectEngineManager: uniform engine lifecycle for all projects ──
|
// ── ProjectEngineManager: uniform engine lifecycle for all projects ──
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user