feat(FN-1225): add headless fn serve command with health checks
- Add a new serve command implementation that runs the dashboard server in headless mode and exposes a health endpoint - Register the serve command in CLI routing and extend bin command coverage for dispatch behavior - Add comprehensive serve command tests for startup flow, options handling, and health-check responses - Fix mission event ordering in MissionStore to keep health snapshots deterministic under concurrent updates - Include a changeset for @gsxdsm/fusion documenting the new fn serve capability
This commit is contained in:
@@ -91,6 +91,33 @@ async function REQUEST(
|
||||
return performRequest(app, method, path, body, headers);
|
||||
}
|
||||
|
||||
describe("createServer health and headless mode", () => {
|
||||
it("returns liveness payload from /api/health", async () => {
|
||||
const store = createMockStore();
|
||||
const app = createServer(store);
|
||||
|
||||
const res = await GET(app, "/api/health");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({
|
||||
status: "ok",
|
||||
version: expect.any(String),
|
||||
uptime: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("serves API routes but no frontend when headless=true", async () => {
|
||||
const store = createMockStore();
|
||||
const app = createServer(store, { headless: true });
|
||||
|
||||
const tasksRes = await GET(app, "/api/tasks");
|
||||
expect(tasksRes.status).toBe(200);
|
||||
|
||||
const rootRes = await GET(app, "/");
|
||||
expect(rootRes.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Error Handling Middleware", () => {
|
||||
let store: TaskStore;
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ const MAX_AI_SESSION_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
export interface ServerOptions {
|
||||
/** Custom merge handler — when provided, used instead of store.mergeTask */
|
||||
onMerge?: (taskId: string) => Promise<MergeResult>;
|
||||
/** When true, run API/websocket server only (skip frontend static assets + SPA fallback) */
|
||||
headless?: boolean;
|
||||
/** Maximum concurrent worktrees / execution slots (default 2) */
|
||||
maxConcurrent?: number;
|
||||
/** Optional GitHub token for PR operations — falls back to GITHUB_TOKEN env var */
|
||||
@@ -142,6 +144,8 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
// Initialize terminal service with project root
|
||||
getTerminalService(store.getRootDir());
|
||||
|
||||
const isHeadless = options?.headless === true;
|
||||
|
||||
// Serve built React app
|
||||
// Resolution order:
|
||||
// 1. FUSION_CLIENT_DIR env override (explicit)
|
||||
@@ -157,7 +161,9 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
? join(__dirname, "..", "dist", "client")
|
||||
: join(__dirname, "..", "client");
|
||||
|
||||
app.use(express.static(clientDir));
|
||||
if (!isHeadless) {
|
||||
app.use(express.static(clientDir));
|
||||
}
|
||||
|
||||
// Rate limiting — stricter limit on SSE connections
|
||||
app.get("/api/events", rateLimit(RATE_LIMITS.sse), async (req, res) => {
|
||||
@@ -375,6 +381,14 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
);
|
||||
}
|
||||
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
version: process.env.npm_package_version ?? "0.4.0",
|
||||
uptime: Math.floor(process.uptime()),
|
||||
});
|
||||
});
|
||||
|
||||
// REST API
|
||||
app.use("/api", createApiRoutes(store, { ...options, aiSessionStore }));
|
||||
|
||||
@@ -397,10 +411,12 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
});
|
||||
|
||||
// SPA fallback
|
||||
app.get("/{*splat}", (_req, res) => {
|
||||
res.sendFile(join(clientDir, "index.html"));
|
||||
});
|
||||
if (!isHeadless) {
|
||||
// SPA fallback
|
||||
app.get("/{*splat}", (_req, res) => {
|
||||
res.sendFile(join(clientDir, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
const dashboardApp = app as DashboardExpressApp;
|
||||
dashboardApp.terminalWsServer = null;
|
||||
|
||||
Reference in New Issue
Block a user