fabro/apps/fabro-web/app/data/runs.test.ts
Bryan Helmkamp 716ba17780
Show billed amount in runs list size tooltip
The Size column in the runs list rendered SizeChip without the billed
total, so its tooltip read "Size M" while the run detail header showed
"Size M · $12.34 billed".

The tooltip was also unreachable: the row title link paints a
`before:absolute before:inset-0` overlay across the whole row, which sat
above the chip and swallowed hover. Wrapping the chip in `relative z-10`
lifts it above that overlay, matching how the created-by and pull request
cells already handle interactive content.

Runs without terminal billing keep the plain "Size M" label, same as the
header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:19:57 -04:00

203 lines
6.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type { Run, RunStatus as ApiRunStatus } from "@qltysh/fabro-api-client";
import {
columnForStatus,
columnStatusDisplay,
isRunStatus,
mapRunListItem,
mapRunToRunItem,
runStatusDisplay,
} from "./runs";
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
function makeRun(overrides: Partial<Run> = {}): Run {
return {
id: "01ABC",
goal: "Fix the build",
title: "Fix the build",
workflow: { slug: "fix_build", name: "Fix Build", graph_name: "FixBuild", node_count: 0, edge_count: 0 },
automation: null,
repository: { name: "myrepo", origin_url: null, provider: "unknown" },
created_by: TEST_PRINCIPAL,
origin: { kind: "api" },
labels: {},
lifecycle: {
status: { kind: "running" },
approval: null,
pending_control: null,
queue_position: null,
error: null,
archived: false,
archived_at: null,
},
sandbox: null,
models: [],
source_directory: "/home/user/myrepo",
timestamps: {
created_at: "2026-04-08T12:00:00Z",
started_at: "2026-04-08T12:00:00Z",
last_event_at: null,
completed_at: null,
},
timing: {
wall_time_ms: 65000,
inference_time_ms: 0,
tool_time_ms: 0,
active_time_ms: 0,
},
billing: { total_usd_micros: 500000 },
size: "XS",
diff: null,
pull_request: null,
current_question: null,
superseded_by: null,
retried_from: null,
links: { web: null },
...overrides,
};
}
function withStatus(status: ApiRunStatus): Pick<Run, "lifecycle"> {
return {
lifecycle: {
status,
approval: null,
pending_control: null,
queue_position: null,
error: null,
archived: false,
archived_at: null,
},
};
}
describe("mapRunListItem", () => {
test("trusts shared server fields for board items", () => {
const summary = makeRun({
title: "Server supplied title",
...withStatus({ kind: "paused", prior_block: null }),
pull_request: {
owner: "fabro-sh",
repo: "fabro",
number: 123,
html_url: "https://github.com/fabro-sh/fabro/pull/123",
},
});
summary.lifecycle.pending_control = "cancel";
const item = mapRunListItem(summary);
expect(item.id).toBe("01ABC");
expect(item.title).toBe("Server supplied title");
expect(item.workflow).toBe("Fix Build");
expect(item.repo).toBe("myrepo");
expect(item.sourceDirectory).toBe("/home/user/myrepo");
expect(item.elapsed).toBeDefined();
expect(item.column).toBe("running");
expect(item.lifecycleStatus).toBe("paused");
expect(item.number).toBe(123);
expect(item.pullRequestUrl).toBe("https://github.com/fabro-sh/fabro/pull/123");
expect(item.pendingControl).toBe("cancel");
});
test("uses a fallback title when the server title is blank", () => {
const summary = makeRun({ id: "01EMPTY", goal: "", title: "" });
expect(mapRunListItem(summary).title).toBe("Untitled run");
});
test("carries the billed total so the size chip can show it on hover", () => {
expect(mapRunListItem(makeRun()).totalUsdMicros).toBe(500000);
});
test("leaves the billed total undefined for runs without terminal billing", () => {
expect(mapRunListItem(makeRun({ billing: null })).totalUsdMicros).toBeUndefined();
expect(
mapRunListItem(makeRun({ billing: { total_usd_micros: null } })).totalUsdMicros,
).toBeUndefined();
});
});
describe("mapRunToRunItem", () => {
test("maps canonical run summary to RunItem", () => {
const summary = makeRun({
pull_request: {
owner: "fabro-sh",
repo: "fabro",
number: 456,
html_url: "https://github.com/fabro-sh/fabro/pull/456",
},
});
const item = mapRunToRunItem(summary);
expect(item.id).toBe("01ABC");
expect(item.title).toBe("Fix the build");
expect(item.workflow).toBe("Fix Build");
expect(item.repo).toBe("myrepo");
expect(item.sourceDirectory).toBe("/home/user/myrepo");
expect(item.elapsed).toBeDefined();
expect(item.lifecycleStatus).toBe("running");
expect(item.number).toBe(456);
expect(item.pullRequestUrl).toBe("https://github.com/fabro-sh/fabro/pull/456");
});
test("handles missing optional fields", () => {
const summary = makeRun({
id: "01DEF",
goal: "",
title: "",
workflow: { slug: null, name: null, graph_name: null, node_count: 0, edge_count: 0 },
source_directory: null,
repository: { name: "unknown", origin_url: null, provider: "unknown" },
...withStatus({ kind: "submitted" }),
timestamps: {
created_at: "2026-04-08T12:00:00Z",
started_at: null,
last_event_at: null,
completed_at: null,
},
timing: null,
billing: null,
});
const item = mapRunToRunItem(summary);
expect(item.id).toBe("01DEF");
expect(item.title).toBe("Untitled run");
expect(item.workflow).toBe("unknown");
expect(item.repo).toBe("unknown");
expect(item.sourceDirectory).toBeUndefined();
});
test("falls back to graph name and slug for workflow labels", () => {
const graphFallback = mapRunToRunItem(
makeRun({ workflow: { slug: "fix_build", name: null, graph_name: "FixBuild", node_count: 0, edge_count: 0 } }),
);
const slugFallback = mapRunToRunItem(
makeRun({ workflow: { slug: "fix_build", name: null, graph_name: null, node_count: 0, edge_count: 0 } }),
);
expect(graphFallback.workflow).toBe("FixBuild");
expect(slugFallback.workflow).toBe("fix_build");
});
test("recognizes canonical blocked, pending, and runnable run statuses", () => {
expect(isRunStatus("pending")).toBe(true);
expect(isRunStatus("runnable")).toBe(true);
expect(isRunStatus("blocked")).toBe(true);
expect(runStatusDisplay).toHaveProperty("pending");
expect(runStatusDisplay).toHaveProperty("runnable");
expect(runStatusDisplay).toHaveProperty("blocked");
});
test("recognizes archived as a terminal run status", () => {
expect(isRunStatus("archived")).toBe(true);
expect(runStatusDisplay).toHaveProperty("archived");
});
test("uses blocked board column instead of waiting", () => {
expect(columnStatusDisplay).toHaveProperty("blocked");
expect(columnStatusDisplay).not.toHaveProperty("waiting");
});
});
describe("columnForStatus", () => {
test("returns null for lifecycle states that do not map to a board column", () => {
expect(columnForStatus("removing")).toBeNull();
});
});