fix(FN-2684): harden system-stats when project lookup fails
- Keep /api/system-stats process and host metrics available even if scoped project resolution throws - Fall back task and agent aggregates to zero when scoped task or agent stores cannot be loaded - Preserve vitest process reporting while guarding optional vitestLastAutoKillAt lookup behind scoped context success - Add route tests for scoped project-not-found fallback behavior and include a patch changeset for @runfusion/fusion
This commit is contained in:
5
.changeset/fix-system-stats-not-found.md
Normal file
5
.changeset/fix-system-stats-not-found.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix `/api/system-stats` so process/system metrics still return when project resolution fails, with task and agent aggregates gracefully falling back to zero counts.
|
||||
@@ -394,6 +394,52 @@ describe("GET /api/system-stats", () => {
|
||||
expect(defaultStore.listTasks).not.toHaveBeenCalled();
|
||||
expect(res.body.taskStats.byColumn.todo).toBe(1);
|
||||
});
|
||||
|
||||
it("returns system stats with zeroed task stats when scoped project resolution fails", async () => {
|
||||
const defaultStore = createMockStore({
|
||||
listTasks: vi.fn().mockResolvedValue([{ id: "FN-default", column: "triage" }]),
|
||||
getFusionDir: vi.fn().mockReturnValue("/fake/default"),
|
||||
});
|
||||
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockRejectedValue(
|
||||
new Error(`Project "${projectId}" not found`),
|
||||
);
|
||||
const initSpy = vi.spyOn(AgentStore.prototype, "init").mockResolvedValue(undefined);
|
||||
const listAgentsSpy = vi.spyOn(AgentStore.prototype, "listAgents").mockResolvedValue([]);
|
||||
|
||||
const res = await GET(buildApp(defaultStore), `/api/system-stats?projectId=${projectId}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(projectStoreResolver.getOrCreateProjectStore).toHaveBeenCalledWith(projectId);
|
||||
expect(defaultStore.listTasks).not.toHaveBeenCalled();
|
||||
expect(initSpy).not.toHaveBeenCalled();
|
||||
expect(listAgentsSpy).not.toHaveBeenCalled();
|
||||
expect(res.body.systemStats).toEqual(
|
||||
expect.objectContaining({
|
||||
rss: expect.any(Number),
|
||||
heapUsed: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
expect(res.body.taskStats).toEqual({
|
||||
total: 0,
|
||||
byColumn: {
|
||||
triage: 0,
|
||||
todo: 0,
|
||||
"in-progress": 0,
|
||||
"in-review": 0,
|
||||
done: 0,
|
||||
archived: 0,
|
||||
},
|
||||
active: 0,
|
||||
agents: {
|
||||
idle: 0,
|
||||
active: 0,
|
||||
running: 0,
|
||||
error: 0,
|
||||
},
|
||||
});
|
||||
expect(res.body.vitestLastAutoKillAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/kill-vitest", () => {
|
||||
|
||||
@@ -1224,23 +1224,13 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
*/
|
||||
router.get("/system-stats", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const mem = process.memoryUsage();
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const load = os.loadavg();
|
||||
const vitestProcessIds = await getVitestProcessIds();
|
||||
|
||||
let vitestLastAutoKillAt: string | null = null;
|
||||
const globalSettingsStore = scopedStore.getGlobalSettingsStore?.();
|
||||
if (globalSettingsStore?.getSettings) {
|
||||
const globalSettings = await globalSettingsStore.getSettings();
|
||||
const candidate = (globalSettings as Record<string, unknown>).vitestLastAutoKillAt;
|
||||
if (typeof candidate === "string" && candidate.length > 0) {
|
||||
vitestLastAutoKillAt = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const tasks = await scopedStore.listTasks({ slim: true, includeArchived: false });
|
||||
let totalTasks = 0;
|
||||
let activeTasks = 0;
|
||||
const byColumn: Record<string, number> = {
|
||||
triage: 0,
|
||||
todo: 0,
|
||||
@@ -1249,20 +1239,43 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
done: 0,
|
||||
archived: 0,
|
||||
};
|
||||
for (const task of tasks) {
|
||||
byColumn[task.column] = (byColumn[task.column] ?? 0) + 1;
|
||||
}
|
||||
|
||||
const { AgentStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
const agents = await agentStore.listAgents();
|
||||
const agentCounts = { idle: 0, active: 0, running: 0, error: 0 };
|
||||
for (const agent of agents) {
|
||||
const state = agent.state as keyof typeof agentCounts;
|
||||
if (state in agentCounts) {
|
||||
agentCounts[state] += 1;
|
||||
let vitestLastAutoKillAt: string | null = null;
|
||||
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
|
||||
const globalSettingsStore = scopedStore.getGlobalSettingsStore?.();
|
||||
if (globalSettingsStore?.getSettings) {
|
||||
const globalSettings = await globalSettingsStore.getSettings();
|
||||
const candidate = (globalSettings as Record<string, unknown>).vitestLastAutoKillAt;
|
||||
if (typeof candidate === "string" && candidate.length > 0) {
|
||||
vitestLastAutoKillAt = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const tasks = await scopedStore.listTasks({ slim: true, includeArchived: false });
|
||||
totalTasks = tasks.length;
|
||||
|
||||
for (const task of tasks) {
|
||||
byColumn[task.column] = (byColumn[task.column] ?? 0) + 1;
|
||||
if (task.column === "in-progress" || task.column === "in-review") {
|
||||
activeTasks += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const { AgentStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
const agents = await agentStore.listAgents();
|
||||
for (const agent of agents) {
|
||||
const state = agent.state as keyof typeof agentCounts;
|
||||
if (state in agentCounts) {
|
||||
agentCounts[state] += 1;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// System stats should still be available even when project resolution/scoped store fails.
|
||||
}
|
||||
|
||||
res.json({
|
||||
@@ -1283,9 +1296,9 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
platform: `${process.platform}/${process.arch}`,
|
||||
},
|
||||
taskStats: {
|
||||
total: tasks.length,
|
||||
total: totalTasks,
|
||||
byColumn,
|
||||
active: tasks.filter((task) => task.column === "in-progress" || task.column === "in-review").length,
|
||||
active: activeTasks,
|
||||
agents: agentCounts,
|
||||
},
|
||||
vitestProcessCount: vitestProcessIds.length,
|
||||
|
||||
Reference in New Issue
Block a user