Skip to content
Merged
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
167 changes: 96 additions & 71 deletions plugins/provider-usage/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ function threadOnMachine(
};
}

const app = await loadPluginApp(() => import("./app"));
const item = app.experimentalSidebarFooterItems[0];
if (item?.kind !== "disclosure") throw new Error("missing disclosure");

describe("provider usage footer disclosure", () => {
it("aggregates every machine and keeps machine and provider selection local to the card", async () => {
const fetchMock = vi.fn(
function createFetchMock() {
return vi.fn(
async (_input: RequestInfo | URL, _init?: RequestInit) =>
new Response(
JSON.stringify({
Expand Down Expand Up @@ -153,54 +157,128 @@ describe("provider usage footer disclosure", () => {
},
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
),
);
vi.stubGlobal("fetch", fetchMock);
const app = await loadPluginApp(() => import("./app"));
const mounted = await mountPluginContentScripts(app, {
pluginId: "provider-usage",
});
const item = app.experimentalSidebarFooterItems[0];
}

it("registers the footer disclosure", () => {
expect(item).toMatchObject({
kind: "disclosure",
id: "usage",
label: "Provider usage",
icon: "ChartColumn",
});
if (item?.kind !== "disclosure") throw new Error("missing disclosure");
});

await waitFor(() =>
expect(fetchMock).toHaveBeenCalledWith(
"/api/v1/plugins/provider-usage/rpc/getUsage",
it("preloads usage and refreshes after an extended focus loss", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
const mounted = await mountPluginContentScripts(app, {
pluginId: "provider-usage",
});
try {
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledWith(
"/api/v1/plugins/provider-usage/rpc/getUsage",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
force: false,
machineIds: null,
maxAgeMs: 30 * 60_000,
}),
}),
),
);

const now = vi.spyOn(Date, "now").mockReturnValue(1_000);
window.dispatchEvent(new Event("blur"));
now.mockReturnValue(5 * 60_000 + 1_001);
const callsBeforeFocus = fetchMock.mock.calls.length;
window.dispatchEvent(new Event("focus"));
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledTimes(callsBeforeFocus + 1),
);
expect(fetchMock.mock.calls.at(-1)?.[1]).toEqual(
expect.objectContaining({
method: "POST",
body: JSON.stringify({
force: false,
machineIds: null,
maxAgeMs: 30 * 60_000,
maxAgeMs: 5 * 60_000,
}),
}),
),
);
);
} finally {
await mounted.lifecycle.dispose();
}
});

it("shows disconnected usage and scopes manual refresh to that machine", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
const dismiss = vi.fn();
const slot = renderSlot(
item,
{ dismiss },
{
context: { threadId: "thread-active" },
sidebarThreads: {
threads: [threadOnMachine("host-intel", "Intel")],
},
},
);
expect(
await slot.findByText(
"Intel is offline. Usage will refresh when it reconnects.",
),
).toBeTruthy();
fireEvent.click(
slot.getByRole("button", { name: "Collapse provider usage" }),
);
expect(dismiss).toHaveBeenCalledOnce();
const reloadButton = slot.getByRole("button", {
name: "Reload provider usage",
}) as HTMLButtonElement;
await waitFor(() => expect(reloadButton.disabled).toBe(false));
const callsBeforeManualRefresh = fetchMock.mock.calls.length;
fireEvent.click(reloadButton);
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledTimes(callsBeforeManualRefresh + 1),
);
expect(fetchMock.mock.calls.at(-1)?.[1]).toEqual(
expect.objectContaining({
body: JSON.stringify({
force: true,
machineIds: ["host-intel"],
maxAgeMs: 0,
}),
}),
);
});

it("aggregates machines and keeps provider selection local to the card", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
const slot = renderSlot(
item,
{ dismiss: vi.fn() },
{
context: { threadId: "thread-active" },
sidebarThreads: {
threads: [threadOnMachine("host-m5", "M5")],
},
},
);
const machinePicker = slot.getByRole("button", {
const machinePicker = await slot.findByRole("button", {
name: "Usage machine: M5",
});
expect(slot.getByRole("heading", { name: "Codex" })).toBeTruthy();
expect(slot.getByText("codex@example.com")).toBeTruthy();
expect(slot.getByText("97% used")).toBeTruthy();

fireEvent.pointerDown(machinePicker, { button: 0 });
expect(slot.getByRole("menuitemradio", { name: "M5" })).toBeTruthy();
expect(slot.getByRole("menuitemradio", { name: "Intel" })).toBeTruthy();
fireEvent.click(slot.getByRole("menuitemradio", { name: "M4" }));
const claudeTab = slot.getByRole("tab", { name: "Claude Code" });
const codexTab = slot.getByRole("tab", { name: "Codex" });
Expand All @@ -225,58 +303,5 @@ describe("provider usage footer disclosure", () => {
expect(slot.getByText("37% used")).toBeTruthy();
fireEvent.keyDown(codexTab, { key: "ArrowLeft" });
expect(claudeTab.getAttribute("aria-selected")).toBe("true");

fireEvent.pointerDown(
slot.getByRole("button", { name: "Usage machine: M4" }),
{ button: 0 },
);
fireEvent.click(slot.getByRole("menuitemradio", { name: "Intel" }));
expect(
slot.getByText(
"Intel is offline. Usage will refresh when it reconnects.",
),
).toBeTruthy();
fireEvent.click(
slot.getByRole("button", { name: "Collapse provider usage" }),
);
expect(dismiss).toHaveBeenCalledOnce();
const reloadButton = slot.getByRole("button", {
name: "Reload provider usage",
}) as HTMLButtonElement;
await waitFor(() => expect(reloadButton.disabled).toBe(false));
const callsBeforeManualRefresh = fetchMock.mock.calls.length;
fireEvent.click(reloadButton);
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledTimes(callsBeforeManualRefresh + 1),
);
expect(fetchMock.mock.calls.at(-1)?.[1]).toEqual(
expect.objectContaining({
body: JSON.stringify({
force: true,
machineIds: ["host-intel"],
maxAgeMs: 0,
}),
}),
);

const now = vi.spyOn(Date, "now").mockReturnValue(1_000);
window.dispatchEvent(new Event("blur"));
now.mockReturnValue(5 * 60_000 + 1_001);
const callsBeforeFocus = fetchMock.mock.calls.length;
window.dispatchEvent(new Event("focus"));
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledTimes(callsBeforeFocus + 1),
);
expect(fetchMock.mock.calls.at(-1)?.[1]).toEqual(
expect.objectContaining({
body: JSON.stringify({
force: false,
machineIds: null,
maxAgeMs: 5 * 60_000,
}),
}),
);

await mounted.lifecycle.dispose();
});
});
Loading