fix(usage): restore policy defaults and isolate usage test state

This commit is contained in:
Mohamed Boudra
2026-10-29 06:48:13 +01:00
parent 54dd93ad7a
commit 6eeb1a0e75
8 changed files with 73 additions and 56 deletions
+36 -29
View File
@@ -1,6 +1,6 @@
import { seedSidebarFooterPreferences } from "../support/helpers/sidebar-nav-settings";
import { expect, test } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import { seedSidebarFooterPreferences } from "../support/helpers/sidebar-nav-settings";
import { installUsageReportsFixture } from "../support/helpers/usage-reports";
import {
claudeAndCodexReports,
@@ -17,34 +17,41 @@ test("Check usage applies to the host and Turn on restores cards", async ({
e2eWorkerClient,
}) => {
test.setTimeout(180_000);
const usage = await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] });
await page.setViewportSize({ width: 1440, height: 1000 });
await seedSidebarFooterPreferences(page, [{ key: "usage", visible: true }]);
await gotoAppShell(page);
await expectPinnedUsage(page, ["31% 5h", "54% wk", "7% 5h", "12% wk"]);
await openUsageFromIcon(page);
await usage.waitForListRequests(1);
await openUsageOptions(page);
await page.getByRole("switch", { name: "Check usage", exact: true }).click();
await closeUsageOptions(page);
await expect(page.getByRole("button", { name: "Turn on", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh all", exact: true })).toHaveCount(0);
expect((await e2eWorkerClient.getDaemonConfig()).config.usage).toEqual({ enabled: false });
await expectNoUsageItem(page);
const before = usage.listRequests().length;
await closeUsage(page);
await page.reload();
await openUsageFromIcon(page);
await expect(page.getByRole("button", { name: "Turn on", exact: true })).toBeVisible();
expect(usage.listRequests()).toHaveLength(before);
await page.getByRole("button", { name: "Turn on", exact: true }).click();
await expect(page.getByText("Claude", { exact: true })).toBeVisible();
expect((await e2eWorkerClient.getDaemonConfig()).config.usage).toEqual({ enabled: true });
// A separate client changes the policy, as another device would.
await e2eWorkerClient.patchDaemonConfig({ usage: { enabled: false } });
await expect(page.getByRole("button", { name: "Turn on", exact: true })).toBeVisible();
await e2eWorkerClient.patchDaemonConfig({ usage: { enabled: true } });
await expect(page.getByText("Claude", { exact: true })).toBeVisible();
const previous = (await e2eWorkerClient.getDaemonConfig()).config.usage ?? { enabled: true };
try {
const usage = await installUsageReportsFixture(page, {
lists: [() => claudeAndCodexReports()],
});
await page.setViewportSize({ width: 1440, height: 1000 });
await seedSidebarFooterPreferences(page, [{ key: "usage", visible: true }]);
await gotoAppShell(page);
await expectPinnedUsage(page, ["31% 5h", "54% wk", "7% 5h", "12% wk"]);
await openUsageFromIcon(page);
await usage.waitForListRequests(1);
await openUsageOptions(page);
await page.getByRole("switch", { name: "Check usage", exact: true }).click();
await closeUsageOptions(page);
await expect(page.getByRole("button", { name: "Turn on", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh all", exact: true })).toHaveCount(0);
expect((await e2eWorkerClient.getDaemonConfig()).config.usage).toEqual({ enabled: false });
await expectNoUsageItem(page);
const before = usage.listRequests().length;
await closeUsage(page);
await page.reload();
await openUsageFromIcon(page);
await expect(page.getByRole("button", { name: "Turn on", exact: true })).toBeVisible();
expect(usage.listRequests()).toHaveLength(before);
await page.getByRole("button", { name: "Turn on", exact: true }).click();
await expect(page.getByText("Claude", { exact: true })).toBeVisible();
expect((await e2eWorkerClient.getDaemonConfig()).config.usage).toEqual({ enabled: true });
// A separate client changes the policy, as another device would.
await e2eWorkerClient.patchDaemonConfig({ usage: { enabled: false } });
await expect(page.getByRole("button", { name: "Turn on", exact: true })).toBeVisible();
await e2eWorkerClient.patchDaemonConfig({ usage: { enabled: true } });
await expect(page.getByText("Claude", { exact: true })).toBeVisible();
} finally {
await e2eWorkerClient.patchDaemonConfig({ usage: previous });
}
});
test("older hosts keep usage without a Check usage switch", async ({ page }) => {
@@ -276,7 +276,7 @@ test.describe("Usage item", () => {
await waitForSettledPosition(sheet);
const sheetBox = (await sheet.boundingBox())!;
// The sheet leaves the screen above it as its backdrop.
expect(sheetBox.y).toBeGreaterThan(0);
expect(sheetBox.y).toBeGreaterThanOrEqual(COMPACT.height * 0.2);
expect(sheetBox.width).toBeGreaterThan(COMPACT.width * 0.8);
await qaScreenshot(page, "compact-sheet");
await closeUsageFromBackdrop(page);
+10 -12
View File
@@ -1,6 +1,3 @@
import { Alert } from "@/components/ui/alert";
import { useUsageSetting } from "./setting";
import { useHosts } from "@/runtime/host-runtime";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Settings } from "lucide-react-native";
@@ -11,7 +8,10 @@ import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-
import { useIsCompactFormFactor } from "@/constants/layout";
import { SettingsRow, SettingsSwitch } from "@/components/settings";
import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control";
import { Alert } from "@/components/ui/alert";
import { useHosts } from "@/runtime/host-runtime";
import { settingsStyles } from "@/styles/settings";
import { useUsageSetting } from "./setting";
import { useUsageDisplay } from "./display";
import { useUsageInSidebar } from "./in-sidebar";
import type { UsageDisplayAs } from "./preferences";
@@ -56,15 +56,13 @@ export function UsageOptions({ serverId }: { serverId: string | null }) {
<View testID="usage-options-fields">
{setting.error ? <Alert variant="error" description={setting.error.message} /> : null}
{setting.supported ? (
<View>
<SettingsSwitch
label={t("providerUsage.checkUsage")}
hint={t("providerUsage.checkUsageHint", { host: hostLabel })}
disabled={!setting.ready}
value={setting.enabled}
onValueChange={setting.setEnabled}
/>
</View>
<SettingsSwitch
label={t("providerUsage.checkUsage")}
hint={t("providerUsage.checkUsageHint", { host: hostLabel })}
disabled={!setting.ready}
value={setting.enabled}
onValueChange={setting.setEnabled}
/>
) : null}
<View style={setting.supported ? settingsStyles.rowBorder : undefined}>
<SettingsSwitch
+6 -3
View File
@@ -1,4 +1,3 @@
import { useUsageSetting } from "./setting";
import { useTranslation } from "react-i18next";
import { supportsUsageReports } from "@getpaseo/client/internal/daemon-client";
import { useCallback, useMemo } from "react";
@@ -18,6 +17,7 @@ import {
useHosts,
} from "@/runtime/host-runtime";
import { useSessionStore, type SessionState } from "@/stores/session-store";
import { useUsageSetting } from "./setting";
import { usageCopy } from "./copy";
import {
replaceReport,
@@ -168,11 +168,14 @@ const NO_REPORTS: UsageReportEntry[] = [];
* The reports of the sidebar's usage host, which is connected and reports usage; none until they
* load, or without a host.
*/
export function useUsageHostReports(serverId: string | null, fetch = true): UsageReportEntry[] {
export function useUsageHostReports(
serverId: string | null,
requestReports = true,
): UsageReportEntry[] {
const setting = useUsageSetting(serverId);
const queryClient = useQueryClient();
const query = useFetchQuery({
enabled: setting.enabled && fetch,
enabled: setting.enabled && requestReports,
queryKey: usageReportsQueryKey(serverId ?? ""),
queryFn: serverId ? () => listReports(queryClient, serverId) : skipToken,
dataShape: "list",
+1 -1
View File
@@ -18,7 +18,7 @@ export function useUsageSetting(serverId: string | null) {
supported,
error: mutation.error,
// COMPAT(usageSetting): added in v0.14, remove after 2027-05-01.
enabled: !supported || config?.usage?.enabled === true,
enabled: !supported || (config !== null && config.usage?.enabled !== false),
ready: !supported || config !== null,
setEnabled,
turnOn,
+5 -4
View File
@@ -1,4 +1,3 @@
import { useUsageInSidebar } from "./in-sidebar";
import {
createContext,
Fragment,
@@ -19,6 +18,7 @@ import {
import { StyleSheet } from "react-native-unistyles";
import { builtinSidebarNavLabelKey } from "@/sidebar-nav/model";
import { useUsageDisplay } from "./display";
import { useUsageInSidebar } from "./in-sidebar";
import { useUsageHostId } from "./hosts";
import { useUsageHostReports } from "./queries";
import { UsageSourceIcon } from "./source-icon";
@@ -34,11 +34,11 @@ import {
import { UsageModal } from "./usage-modal";
/** Each summary window with data on the usage host, under its source; empty while none has. */
function useUsageSummary(fetch = true): readonly PinnedUsageSource[] {
function useUsageSummary(requestReports = true): readonly PinnedUsageSource[] {
const { t } = useTranslation();
const { inSidebar } = useUsageInSidebar();
const serverId = useUsageHostId();
const reports = useUsageHostReports(inSidebar ? serverId : null, fetch);
const reports = useUsageHostReports(inSidebar ? serverId : null, requestReports);
const { preferences } = useUsageDisplay(reports);
return useMemo(() => resolvePinnedUsage(reports, preferences, t), [preferences, reports, t]);
}
@@ -57,7 +57,8 @@ export function UsageSidebarItem() {
const { t } = useTranslation();
const label = t(builtinSidebarNavLabelKey("usage"));
const openUsage = useOpenSidebarUsage();
// The footer owns fetching; this row reads the same cached summary.
// The row mounts after reports arrive; observing without requesting avoids
// the fetch-query policy’s unconditional refetch on mount.
const sources = useUsageSummary(false);
if (sources.length === 0) return null;
return <PinnedUsageTrigger label={label} sources={sources} onPress={openUsage} />;
+1 -1
View File
@@ -1,4 +1,3 @@
import { useUsageSetting } from "./setting";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { Text, View } from "react-native";
@@ -8,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { settingsStyles } from "@/styles/settings";
import { SettingsSection } from "@/components/settings/headings/settings-section";
import type { UsageDisplay } from "./display";
import { useUsageSetting } from "./setting";
import { UsageControls } from "./controls";
import { UsageList } from "./list";
import type { UsageView } from "./types";
@@ -51,8 +51,19 @@ export class UsageSession {
async handleLegacyList(
msg: Extract<SessionInboundMessage, { type: "provider.usage.list.request" }>,
): Promise<void> {
if (!this.options.isEnabled()) {
this.options.emit({
type: "rpc_error",
payload: {
requestId: msg.requestId,
requestType: msg.type,
error: "Usage is off on this host.",
code: "provider_usage_list_failed",
},
});
return;
}
try {
if (!this.options.isEnabled()) throw new Error("Usage is off on this host.");
if (!this.options.runtime) throw new Error("Plugin runtime is unavailable");
const usage = await this.options.runtime.listLegacyUsage();
this.options.emit({
@@ -71,10 +82,7 @@ export class UsageSession {
payload: {
requestId: msg.requestId,
requestType: msg.type,
error:
err.message === "Usage is off on this host."
? err.message
: `Failed to list provider usage: ${err.message}`,
error: `Failed to list provider usage: ${err.message}`,
code: "provider_usage_list_failed",
},
});