Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { AutoresearchRun } from "@posthog/core/autoresearch/schemas";
import type { AcpMessage } from "@posthog/shared";
import { Theme } from "@radix-ui/themes";
import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AutoresearchObservability } from "./AutoresearchObservability";

const STARTED_AT = 1_000;

function makeRun(overrides: Partial<AutoresearchRun> = {}): AutoresearchRun {
return {
id: "run-1",
config: {
taskId: "task-1",
direction: "minimize",
targetValue: null,
maxIterations: 10,
implementModel: null,
measureModel: null,
implementEffort: null,
measureEffort: null,
instructions: "Reduce memory usage.",
},
status: "running",
metricName: null,
metricUnit: null,
phase: null,
originalModel: null,
originalEffort: null,
researchFindings: [],
iterations: [],
startedAt: STARTED_AT,
endedAt: null,
endReason: null,
interruptedReason: null,
lastError: null,
...overrides,
};
}

function toolCall(ts: number, kind: string): AcpMessage {
return {
type: "acp_message",
ts,
message: {
jsonrpc: "2.0",
method: "session/update",
params: {
update: {
sessionUpdate: "tool_call",
title: `tool at ${ts}`,
kind,
status: "completed",
},
},
},
} as AcpMessage;
}

function renderObservability(run: AutoresearchRun, events: AcpMessage[]) {
return render(
<Theme>
<AutoresearchObservability run={run} events={events} />
</Theme>,
);
}

describe("AutoresearchObservability", () => {
afterEach(() => vi.useRealTimers());

it("keeps observed-time bars within range when a kind outlasts wall-clock", () => {
// Live run where the latest tool timestamp sits ahead of `now` (clock skew
// between agent-set event timestamps and the client clock). The activity
// analysis then attributes more elapsed time to one kind than the run's
// wall-clock duration, which used to push Progress past its max of 100.
vi.useFakeTimers();
vi.setSystemTime(STARTED_AT + 10_000);

renderObservability(makeRun(), [
toolCall(STARTED_AT, "edit"),
toolCall(STARTED_AT + 25_000, "execute"),
]);

const bars = screen.getAllByRole("progressbar");
expect(bars).toHaveLength(4);
for (const bar of bars) {
const now = bar.getAttribute("aria-valuenow");
// A rejected (out-of-range) value renders no aria-valuenow at all.
expect(now).not.toBeNull();
const value = Number(now);
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThanOrEqual(100);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,12 @@ function TimeRow({
value: number;
total: number;
}) {
const percentage = Math.round((value / total) * 100);
// Per-kind observed time can exceed the run's wall-clock duration, so clamp
// before handing the value to Progress (whose max defaults to 100).
const percentage =
total > 0
? Math.min(100, Math.max(0, Math.round((value / total) * 100)))
: 0;
return (
<div>
<div className="mb-1 flex items-center justify-between gap-3">
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/features/billing/UsageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ export function UsageButton() {
usage.sustained.used_percent >= usage.burst.used_percent
? usage.sustained
: usage.burst;
const usagePercent = Math.min(Math.round(dominant.used_percent), 100);
const usagePercent = Math.min(
Math.max(Math.round(dominant.used_percent), 0),
100,
);
const resetLabel = formatResetTime(dominant.reset_at);

const handleOpenChange = (nextOpen: boolean) => {
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/features/billing/UsageMeter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ interface UsageMeterProps {

export function UsageMeter({ label, bucket, color }: UsageMeterProps) {
const percentage = bucket.used_percent;
// used_percent can exceed 100 once a limit is blown; the text still shows the
// true figure, but Progress rejects values above its default max of 100.
const progressValue = Math.min(100, Math.max(0, percentage));

const borderColor = color === "red" ? "var(--red-7)" : "var(--gray-5)";

Expand All @@ -28,7 +31,7 @@ export function UsageMeter({ label, bucket, color }: UsageMeterProps) {
<Text className="font-medium text-sm">{percentage.toFixed(2)}%</Text>
</Flex>
<Progress
value={percentage}
value={progressValue}
size="2"
color={color === "red" ? "red" : undefined}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/features/updates/UpdateAvailableModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function UpdateAvailableModal() {
enabled: isOpen || prefetchForActiveUpdate,
});

const percent = Math.round(downloadPercent ?? 0);
const percent = Math.min(100, Math.max(0, Math.round(downloadPercent ?? 0)));
const sizeLabel = formatSize(downloadSizeBytes);
const isDownloading = status === "downloading";
const isReady = status === "ready" || status === "installing";
Expand Down
Loading