diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index 2ac8658d..0f02c2ff 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -344,7 +344,7 @@ describe("agent live run routes", () => { expect(mockHeartbeatService.buildRunOutputSilence).toHaveBeenCalledTimes(50); }); - it("treats explicit zero live run limits as the capped default", async () => { + it("treats explicit zero or invalid live run limit as the capped default", async () => { const rows = Array.from({ length: 75 }, (_, index) => ({ id: `run-${index}`, companyId: "company-1", @@ -381,4 +381,143 @@ describe("agent live run routes", () => { expect(limit).toHaveBeenCalledWith(50); expect(res.body).toHaveLength(50); }); + + it("does not pad with recent runs when no minCount is requested", async () => { + const liveRows = Array.from({ length: 8 }, (_, index) => ({ + id: `run-live-${index}`, + companyId: "company-1", + status: "running", + invocationSource: "on_demand", + triggerDetail: "manual", + startedAt: new Date("2026-04-10T09:30:00.000Z"), + finishedAt: null, + createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + agentId: "agent-1", + agentName: "Builder", + adapterType: "codex_local", + logBytes: 0, + livenessState: "healthy", + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, + lastOutputAt: null, + lastOutputSeq: null, + lastOutputStream: null, + lastOutputBytes: 0, + processStartedAt: null, + issueId: "issue-1", + })); + + const selectCalls: Array> = []; + const db = { + select: vi.fn().mockImplementation(() => { + const limitFn = vi.fn(async (value: number) => liveRows.slice(0, value)); + const orderedQuery = { + limit: limitFn, + then: (resolve: (value: typeof liveRows) => unknown) => + Promise.resolve(liveRows).then(resolve), + }; + const query = { + from: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnValue(orderedQuery), + }; + selectCalls.push(limitFn); + return query; + }), + }; + + const res = await requestApp( + await createApp(db), + (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs"), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(res.body).toHaveLength(8); + expect(db.select).toHaveBeenCalledTimes(1); + }); + + it("pads with recent runs when minCount is explicitly requested", async () => { + const liveRows = Array.from({ length: 2 }, (_, index) => ({ + id: `run-live-${index}`, + companyId: "company-1", + status: "running", + invocationSource: "on_demand", + triggerDetail: "manual", + startedAt: new Date("2026-04-10T09:30:00.000Z"), + finishedAt: null, + createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + agentId: "agent-1", + agentName: "Builder", + adapterType: "codex_local", + logBytes: 0, + livenessState: "healthy", + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, + lastOutputAt: null, + lastOutputSeq: null, + lastOutputStream: null, + lastOutputBytes: 0, + processStartedAt: null, + issueId: "issue-1", + })); + const recentRows = Array.from({ length: 4 }, (_, index) => ({ + id: `run-recent-${index}`, + companyId: "company-1", + status: "succeeded", + invocationSource: "on_demand", + triggerDetail: "manual", + startedAt: new Date("2026-04-09T09:30:00.000Z"), + finishedAt: new Date("2026-04-09T09:35:00.000Z"), + createdAt: new Date(`2026-04-09T09:${String(index % 60).padStart(2, "0")}:00.000Z`), + agentId: "agent-1", + agentName: "Builder", + adapterType: "codex_local", + logBytes: 0, + livenessState: "healthy", + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, + lastOutputAt: null, + lastOutputSeq: null, + lastOutputStream: null, + lastOutputBytes: 0, + processStartedAt: null, + issueId: "issue-1", + })); + + let selectCallCount = 0; + const db = { + select: vi.fn().mockImplementation(() => { + selectCallCount += 1; + const rows = selectCallCount === 1 ? liveRows : recentRows; + const limitFn = vi.fn(async (value: number) => rows.slice(0, value)); + const orderedQuery = { + limit: limitFn, + then: (resolve: (value: typeof rows) => unknown) => + Promise.resolve(rows).then(resolve), + }; + return { + from: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnValue(orderedQuery), + }; + }), + }; + + const res = await requestApp( + await createApp(db), + (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs?minCount=4"), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(res.body).toHaveLength(4); + expect(db.select).toHaveBeenCalledTimes(2); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 6b663a85..1f1ee3e8 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -2844,7 +2844,12 @@ export function agentRoutes( const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); - const minCount = readLiveRunsQueryInt(req.query.minCount, 50, 50); + // `minCount` is a padding floor for callers that want a minimum number of + // recent runs to render (e.g. dashboard cards). It must default to 0 so + // callers asking for "live runs" get only actually-live runs — otherwise + // every caller with no minCount param gets up to 50 historical runs + // padded in and renders bogus "live" counts. + const minCount = readLiveRunsQueryInt(req.query.minCount, 50, 0); const limit = readLiveRunsQueryInt(req.query.limit, 50, 50); const columns = {