[PAP-3154] Stop padding /live-runs by default (#4963)
## Summary - Fix [PAP-3154](/PAP/issues/PAP-3154): the Sidebar's "Dashboard NN live" badge showed a constant 50 in every company because `GET /api/companies/:companyId/live-runs` was padding its response with up to 50 recent (non-live) heartbeat runs whenever the caller did not pass `minCount`. - Regression introduced by [#4875](https://github.com/paperclipai/paperclip/pull/4875) (commit `6445bef9`), which capped both `minCount` and `limit` at 50 with a fallback of 50 for omitted values. The cap is correct for `limit` (real unboundedness guard); for `minCount` it conflates "no padding" with "pad to the cap". - Default `minCount` to 0 so callers asking for "live runs" only get actually-live runs unless they explicitly request padding (`ActiveAgentsPanel` is the only caller that does). Keep `limit` capped at 50 by default. ## Test plan - [x] `pnpm exec vitest run server/src/__tests__/agent-live-run-routes.test.ts` — 7/7 pass, including new tests for the no-pad default and explicit padding. - [x] `pnpm exec vitest run ui/src/components/Sidebar.test.tsx ui/src/components/ActiveAgentsPanel.test.tsx ui/src/api/heartbeats.test.ts` — 6/6 pass. - [ ] Verify in dev: with ~8 truly-live runs in a company, the sidebar Dashboard badge shows the real count (not 50). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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<ReturnType<typeof vi.fn>> = [];
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user