feat(FN-4785): complete Step 3 — batch agent run status aggregation
Fusion-Task-Id: FN-4785 Fusion-Task-Lineage: be241ee7-9df0-433c-a00b-997bcd558862
This commit is contained in:
committed by
gsxdsm
parent
7aeb7583f4
commit
be54c75533
@@ -2108,6 +2108,38 @@ export class AgentStore extends EventEmitter {
|
||||
.filter((run): run is AgentHeartbeatRun => run !== null);
|
||||
}
|
||||
|
||||
async getRunStatusCounts(agentIds?: readonly string[]): Promise<{ completedRuns: number; failedRuns: number }> {
|
||||
let rows: Array<{ status: string; count: number }>;
|
||||
|
||||
if (agentIds && agentIds.length > 0) {
|
||||
const placeholders = agentIds.map(() => "?").join(",");
|
||||
rows = this.db.prepare(`
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM agentRuns
|
||||
WHERE agentId IN (${placeholders})
|
||||
GROUP BY status
|
||||
`).all(...agentIds) as Array<{ status: string; count: number }>;
|
||||
} else {
|
||||
rows = this.db.prepare(`
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM agentRuns
|
||||
GROUP BY status
|
||||
`).all() as Array<{ status: string; count: number }>;
|
||||
}
|
||||
|
||||
let completedRuns = 0;
|
||||
let failedRuns = 0;
|
||||
for (const row of rows) {
|
||||
if (row.status === "completed") {
|
||||
completedRuns += row.count;
|
||||
} else if (row.status === "failed" || row.status === "terminated") {
|
||||
failedRuns += row.count;
|
||||
}
|
||||
}
|
||||
|
||||
return { completedRuns, failedRuns };
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Run-scoped log storage (JSONL files alongside run JSON in agentsDir)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -170,8 +170,21 @@ function createMockGlobalSettingsStore() {
|
||||
}
|
||||
|
||||
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
const getTask = vi.fn();
|
||||
const getTaskColumns = vi.fn(async (ids: string[]) => {
|
||||
const columns = new Map<string, string>();
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const task = await getTask(id);
|
||||
if (task?.column) columns.set(id, task.column);
|
||||
}),
|
||||
);
|
||||
return columns;
|
||||
});
|
||||
|
||||
return {
|
||||
getTask: vi.fn(),
|
||||
getTask,
|
||||
getTaskColumns,
|
||||
listTasks: vi.fn().mockResolvedValue([]),
|
||||
searchTasks: vi.fn().mockResolvedValue([]),
|
||||
createTask: vi.fn(),
|
||||
@@ -4048,18 +4061,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const doneTaskId = "FN-DONE";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: doneTaskId,
|
||||
column: "done",
|
||||
description: "Completed task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Done\n\nDone task",
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[doneTaskId, "done"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4085,18 +4087,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const archivedTaskId = "FN-ARCHIVED";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: archivedTaskId,
|
||||
column: "archived",
|
||||
description: "Archived task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Archived\n\nArchived task",
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[archivedTaskId, "archived"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4122,18 +4113,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const activeTaskId = "FN-ACTIVE";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: activeTaskId,
|
||||
column: "in-progress",
|
||||
description: "Active task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Active\n\nActive task",
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[activeTaskId, "in-progress"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4159,18 +4139,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const doneTaskId = "FN-DONE-DETAIL";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: doneTaskId,
|
||||
column: "done",
|
||||
description: "Done task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Done\n\nDone task",
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[doneTaskId, "done"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4195,18 +4164,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const archivedTaskId = "FN-ARCHIVED-DETAIL";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: archivedTaskId,
|
||||
column: "archived",
|
||||
description: "Archived task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Archived\n\nArchived task",
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[archivedTaskId, "archived"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4231,18 +4189,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const inReviewTaskId = "FN-IN-REVIEW";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: inReviewTaskId,
|
||||
column: "in-review",
|
||||
description: "In review task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# In Review\n\nIn review task",
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[inReviewTaskId, "in-review"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4269,37 +4216,12 @@ describe("Agent stale task-link sanitization", () => {
|
||||
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockImplementation(async (taskId: string) => {
|
||||
if (taskId === doneTaskId) {
|
||||
return {
|
||||
id: doneTaskId,
|
||||
column: "done",
|
||||
description: "Done task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Done\n\nDone task",
|
||||
};
|
||||
}
|
||||
if (taskId === activeTaskId) {
|
||||
return {
|
||||
id: activeTaskId,
|
||||
column: "in-progress",
|
||||
description: "Active task",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# Active\n\nActive task",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
getTaskColumns: vi.fn().mockResolvedValue(
|
||||
new Map([
|
||||
[doneTaskId, "done"],
|
||||
[activeTaskId, "in-progress"],
|
||||
]),
|
||||
),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
@@ -4324,6 +4246,51 @@ describe("Agent stale task-link sanitization", () => {
|
||||
expect(res.body.assignedTaskCount).toBe(1);
|
||||
});
|
||||
|
||||
it("GET /api/agents does not call getTask when sanitizing linked taskIds", async () => {
|
||||
const taskId = "FN-PERF-GUARD";
|
||||
const getTaskSpy = vi.fn();
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: getTaskSpy,
|
||||
getTaskColumns: vi.fn().mockResolvedValue(new Map([[taskId, "in-progress"]])),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
|
||||
const agentStore = new AgentStore({ rootDir: fusionDir });
|
||||
await agentStore.init();
|
||||
await agentStore.assignTask(agentId, taskId);
|
||||
|
||||
const res = await GET(app, "/api/agents");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(getTaskSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("GET /api/agents/stats does not call getRecentRuns per agent", async () => {
|
||||
const getRecentRunsSpy = vi.spyOn(AgentStore.prototype, "getRecentRuns");
|
||||
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
|
||||
const agentStore = new AgentStore({ rootDir: fusionDir });
|
||||
await agentStore.init();
|
||||
await agentStore.createAgent({ name: "Stats Agent 1", role: "executor" });
|
||||
await agentStore.createAgent({ name: "Stats Agent 2", role: "executor" });
|
||||
|
||||
const res = await GET(app, "/api/agents/stats");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(getRecentRunsSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("GET /api/agents/stats preserves taskId for agents with no linked task", async () => {
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
@@ -4386,7 +4353,7 @@ describe("Agent stale task-link sanitization", () => {
|
||||
const taskId = "FN-LOOKUP-FAIL";
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
getTask: vi.fn().mockRejectedValue(new Error("Database error")),
|
||||
getTaskColumns: vi.fn().mockRejectedValue(new Error("Database error")),
|
||||
} as any);
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -317,13 +317,7 @@ export function registerAgentCoreRoutes(ctx: ApiRoutesContext, deps: AgentCoreRo
|
||||
const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
|
||||
const assignedTaskCount = sanitizedAgents.filter((a) => a.taskId).length;
|
||||
|
||||
let completedRuns = 0;
|
||||
let failedRuns = 0;
|
||||
for (const agent of agents) {
|
||||
const runs = await agentStore.getRecentRuns(agent.id, 100);
|
||||
completedRuns += runs.filter((r) => r.status === "completed").length;
|
||||
failedRuns += runs.filter((r) => r.status === "failed" || r.status === "terminated").length;
|
||||
}
|
||||
const { completedRuns, failedRuns } = await agentStore.getRunStatusCounts();
|
||||
|
||||
const total = completedRuns + failedRuns;
|
||||
const successRate = total > 0 ? completedRuns / total : 0;
|
||||
|
||||
Reference in New Issue
Block a user