mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
[PAP-3154] Stop padding /live-runs by default
The /companies/:companyId/live-runs route was inflating its response by
padding with up to 50 recent (non-live) heartbeat runs whenever a caller
didn't pass `minCount`. The Sidebar's "Dashboard NN live" badge reads
the array length from this endpoint, so every company saw "50 live" even
when only a handful of runs were actually queued or running.
Regression introduced by 6445bef9 ("Harden issue recovery reliability"),
which capped both `minCount` and `limit` at 50 with a fallback of 50
including for omitted values. The cap is correct for `limit` (a real
unboundedness guard); for `minCount` it conflates "no padding" with
"pad to the cap" and is what produced the bogus counts.
- Default `minCount` to 0 so callers asking for "live runs" only get
actually-live runs unless they explicitly request padding.
- Keep `limit` capped at 50 by default to preserve the bound on response
size.
- Add tests for the no-pad default and for explicit padding behavior so
the contract stays clear.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
42a299fb9d
commit
5d70cdfbe5
2 files changed
+146
-2
No files matched your search
@@ -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