Files
multica/packages/core/api/agent-activity.test.ts
f639d35f44 fix(agents): MUL-7383 exclude cancelled runs from success metrics (#8424)
* fix(agents): exclude cancelled runs from success metrics

* refactor(agents): require activity outcome counts from the back-end

The previous commit carried a fallback for a new client talking to an
older back-end: optional `completed_count` / `cancelled_count`, null
propagation through the bucket series, and a "—" success rate when the
counts were absent. Multica only supports the other direction — an older
daemon or client against a newer back-end — so that branch is an
unrequested legacy adapter.

Outcome counts are now required at the schema boundary, so the bucket
series and window summary carry plain numbers. A response without them
is contract drift and `parseWithFallback` degrades the whole list to
`[]`, the same as every other list endpoint.

Unchanged: a window holding nothing but cancellations still has no
success rate and renders "—" rather than claiming 100%, and the
sparkline still draws cancellations neutral. Both are the point of the
fix, not compatibility. Copy for the unavailable state now says the
period has no completed or failed run, which is the only way it is
reachable.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-15 13:22:07 +08:00

46 lines
1.4 KiB
TypeScript

// @vitest-environment node
import { afterEach, describe, expect, it, vi } from "vitest";
import { ApiClient } from "./client";
const bucket = {
agent_id: "a",
bucket_at: "2026-09-15T00:00:00Z",
task_count: 10,
failed_count: 1,
};
afterEach(() => vi.unstubAllGlobals());
async function read(body: unknown) {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(new Response(JSON.stringify(body), { status: 200 })),
);
return new ApiClient("https://api.example.test").getWorkspaceAgentActivity30d();
}
describe("agent activity API outcomes", () => {
it("preserves explicit outcome counts, including zero", async () => {
expect(await read([{ ...bucket, completed_count: 0, cancelled_count: 9 }])).toEqual([
{ ...bucket, completed_count: 0, cancelled_count: 9 },
]);
});
// Outcome counts are part of the contract, so a bucket without usable
// ones is drift, not an older peer to accommodate. Degrading the whole
// response is the intended failure mode: an empty activity panel is
// honest, a half-populated one silently misreports success.
it.each([undefined, null, "1", -1, 1.5])(
"drops the response when an outcome count is %s",
async (bad) => {
expect(
await read([{ ...bucket, completed_count: bad, cancelled_count: bad }]),
).toEqual([]);
},
);
it("falls back for a malformed list", async () => {
expect(await read({ buckets: [] })).toEqual([]);
});
});