fix(ui): stop expanding stale cached usage tiers on ineligible provider cards

A disabled React Query observer still returns the existing cache entry for
the same key, so a provider switched to the official subscription template
(or with its usage script disabled) kept showing the multi-plan chevron and
the expanded tiers left over from a previous script result.

Gate hasMultiplePlans with the same predicate that enables the usage query,
extracted as scriptUsageActive so the two cannot drift apart again.
This commit is contained in:
Jason committed 2026-09-21 21:45:23 +08:00
1 parent 8272707d5e
commit 2c735bd9de
2 files changed
+117 -2

No files matched your search

+11 -2
View File
@@ -317,15 +317,24 @@ export function ProviderCard({
? provider.meta?.usage_script?.autoQueryInterval || 0
: 0;
// 脚本用量只在「已启用 + 非官方 + 非官方订阅模板」时才查询;展开判定必须复用同一谓词,
// 因为禁用的 React Query observer 仍会返回同 key 的旧缓存。
const scriptUsageActive =
usageEnabled && !isOfficial && !isOfficialSubscriptionUsage;
const { data: usage } = useUsageQuery(provider.id, appId, {
enabled: usageEnabled && !isOfficial && !isOfficialSubscriptionUsage,
enabled: scriptUsageActive,
autoQueryInterval,
});
const isTokenPlan =
provider.meta?.usage_script?.templateType === "token_plan";
// 官方订阅的额度窗口不能按普通多套餐展开;缓存残留的旧脚本结果同样不认。
const hasMultiplePlans =
usage?.success && usage.data && usage.data.length > 1 && !isTokenPlan;
scriptUsageActive &&
!isTokenPlan &&
usage?.success &&
usage.data &&
usage.data.length > 1;
const [isExpanded, setIsExpanded] = useState(false);
@@ -0,0 +1,106 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ProviderCard } from "@/components/providers/ProviderCard";
import type { TemplateType } from "@/config/constants";
import { usageKeys } from "@/lib/query/usage";
import type { Provider } from "@/types";
import { createTestQueryClient } from "../utils/testQueryClient";
vi.mock("@/components/providers/ProviderActions", () => ({
ProviderActions: () => null,
}));
vi.mock("@/components/ProviderIcon", () => ({ ProviderIcon: () => null }));
vi.mock("@/components/UsageFooter", () => ({
default: ({ inline }: { inline: boolean }) =>
inline ? null : <div>expanded-plan-details</div>,
}));
vi.mock("@/components/SubscriptionQuotaFooter", () => ({
default: () => <div>official-subscription-quota</div>,
}));
vi.mock("@/lib/query/failover", () => ({
useProviderHealth: () => ({ data: undefined }),
}));
function renderCard({
official = false,
templateType = "custom",
enabled = true,
}: {
official?: boolean;
templateType?: TemplateType;
enabled?: boolean;
} = {}) {
const provider: Provider = {
id: "cached-usage-provider",
name: "Usage provider",
category: official ? "official" : "custom",
settingsConfig: {
env: { ANTHROPIC_BASE_URL: "https://example.com" },
},
meta: {
usage_script: { enabled, language: "javascript", code: "", templateType },
},
};
const queryClient = createTestQueryClient();
// A disabled React Query observer still receives existing cache entries.
queryClient.setQueryData(usageKeys.script(provider.id, "claude"), {
success: true,
data: [
{ planName: "five_hour", total: 100, used: 0, remaining: 100, unit: "%" },
{ planName: "seven_day", total: 100, used: 30, remaining: 70, unit: "%" },
],
});
return render(
<QueryClientProvider client={queryClient}>
<ProviderCard
provider={provider}
appId="claude"
isCurrent={true}
isProxyRunning={false}
onSwitch={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfigureUsage={vi.fn()}
onOpenWebsite={vi.fn()}
onDuplicate={vi.fn()}
/>
</QueryClientProvider>,
);
}
describe("ProviderCard cached usage expansion", () => {
it("keeps official subscription quota inline despite cached script tiers", () => {
renderCard({ official: true, templateType: "official_subscription" });
expect(screen.getByText("official-subscription-quota")).toBeInTheDocument();
expect(screen.queryByText("expanded-plan-details")).not.toBeInTheDocument();
expect(screen.queryByTitle("收起")).not.toBeInTheDocument();
expect(screen.queryByTitle("展开")).not.toBeInTheDocument();
});
it.each<Parameters<typeof renderCard>[0]>([
{ official: true },
{ templateType: "official_subscription" },
{ templateType: "token_plan" },
{ enabled: false },
])("does not expand ineligible cached usage: %j", (options) => {
renderCard(options);
expect(screen.queryByText("expanded-plan-details")).not.toBeInTheDocument();
expect(screen.queryByTitle("收起")).not.toBeInTheDocument();
expect(screen.queryByTitle("展开")).not.toBeInTheDocument();
});
it("still expands ordinary multi-plan usage and allows collapsing it", async () => {
const user = userEvent.setup();
renderCard();
expect(screen.getByText("expanded-plan-details")).toBeInTheDocument();
await user.click(screen.getByTitle("收起"));
expect(screen.queryByText("expanded-plan-details")).not.toBeInTheDocument();
expect(screen.getByTitle("展开")).toBeInTheDocument();
});
});