Merge main and harden managed Codex account flow

This commit is contained in:
SaladDay
2026-07-18 10:06:58 +00:00
276 changed files with 32101 additions and 3292 deletions
@@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { AddProviderDialog } from "@/components/providers/AddProviderDialog";
import type { ProviderFormValues } from "@/components/providers/forms/ProviderForm";
import { codexProviderPresets } from "@/config/codexProviderPresets";
vi.mock("@/components/ui/dialog", () => ({
Dialog: ({ children }: { children: React.ReactNode }) => (
@@ -125,4 +126,96 @@ describe("AddProviderDialog", () => {
},
});
});
it("submits the managed account selected from the Codex Official preset", async () => {
const handleSubmit = vi.fn().mockResolvedValue(undefined);
const officialPresetIndex = codexProviderPresets.findIndex(
(preset) =>
preset.category === "official" && preset.providerType === "codex_oauth",
);
expect(officialPresetIndex).toBeGreaterThanOrEqual(0);
mockFormValues = {
name: "OpenAI Official",
websiteUrl: "https://chatgpt.com/codex",
settingsConfig: JSON.stringify({ auth: {}, config: "" }),
presetId: `codex-${officialPresetIndex}`,
presetCategory: "official",
meta: {
providerType: "codex_oauth",
authBinding: {
source: "managed_account",
authProvider: "codex_oauth",
accountId: "acct-managed",
},
},
};
render(
<AddProviderDialog
open
onOpenChange={vi.fn()}
appId="codex"
onSubmit={handleSubmit}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "common.add" }));
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
expect(handleSubmit).toHaveBeenCalledWith(
expect.objectContaining({
category: "official",
ensureCodexOfficialSeed: true,
meta: expect.objectContaining({
authBinding: {
source: "managed_account",
authProvider: "codex_oauth",
accountId: "acct-managed",
},
}),
}),
);
});
it("新建 Grok Build 自定义供应商时不补默认 Grok 图标", async () => {
const handleSubmit = vi.fn().mockResolvedValue(undefined);
mockFormValues = {
name: "tes 1",
websiteUrl: "",
icon: "",
iconColor: "",
settingsConfig: JSON.stringify({
config: `[models]
default = "grok-4.5"
[model."grok-4.5"]
model = "grok-4.5"
base_url = "https://grok.example.com/v1"
name = "tes 1"
api_key = "secret"
api_backend = "responses"
context_window = 500000
`,
}),
};
render(
<AddProviderDialog
open
onOpenChange={vi.fn()}
appId="grokbuild"
onSubmit={handleSubmit}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "common.add" }));
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
const submitted = handleSubmit.mock.calls[0][0];
expect(submitted.icon).toBeUndefined();
expect(submitted.iconColor).toBeUndefined();
});
});
@@ -173,7 +173,7 @@ describe("ClaudeDesktopProviderForm", () => {
// claude-old 迁移到 Sonnet;留空的 Opus / Fable / Haiku 回填为 Sonnet 的
// 上游模型,保证落库四档齐全,子 agent 调用的各档始终可解析。
expect(submitted.meta.claudeDesktopModelRoutes).toMatchObject({
"claude-sonnet-4-6": {
"claude-sonnet-5": {
model: "upstream-old",
labelOverride: "upstream-old",
},
@@ -186,7 +186,7 @@ describe("ClaudeDesktopProviderForm", () => {
"claude-fable-5",
"claude-haiku-4-5",
"claude-opus-4-8",
"claude-sonnet-4-6",
"claude-sonnet-5",
],
);
});
@@ -217,7 +217,7 @@ describe("ClaudeDesktopProviderForm", () => {
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
const routes = onSubmit.mock.calls[0][0].meta.claudeDesktopModelRoutes;
// 留空的 Opus / Haiku 回填同一上游模型,1M 声明应与 Sonnet 一致。
expect(routes["claude-sonnet-4-6"]).toMatchObject({
expect(routes["claude-sonnet-5"]).toMatchObject({
model: "deepseek-v4-pro",
supports1m: true,
});
@@ -259,8 +259,8 @@ describe("ClaudeDesktopProviderForm", () => {
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
const submitted = onSubmit.mock.calls[0][0];
expect(submitted.meta.claudeDesktopModelRoutes).toMatchObject({
"claude-sonnet-4-6": {
model: "claude-sonnet-4-6",
"claude-sonnet-5": {
model: "claude-sonnet-5",
},
});
});
@@ -85,6 +85,7 @@ const renderCopilotForm = (overrides: Partial<ClaudeFormFieldsProps> = {}) => {
defaultOpusModelName: "",
defaultFableModel: "",
defaultFableModelName: "",
subagentModel: "",
onModelChange: vi.fn(),
speedTestEndpoints: [],
apiFormat: "anthropic",
@@ -173,4 +174,25 @@ describe("ClaudeFormFields", () => {
);
});
});
it("一键设置会同时写入 Subagent 模型", () => {
const onModelChange = vi.fn();
renderCopilotForm({
claudeModel: "shared-model[1M]",
defaultSonnetModel: "",
defaultSonnetModelName: "",
onModelChange,
});
fireEvent.click(
screen.getByRole("button", {
name: "一键设置",
}),
);
expect(onModelChange).toHaveBeenCalledWith(
"CLAUDE_CODE_SUBAGENT_MODEL",
"shared-model[1M]",
);
});
});
+33
View File
@@ -0,0 +1,33 @@
import { render } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
const Panels = ({ innerOpen }: { innerOpen: boolean }) => (
<>
<FullScreenPanel isOpen title="Outer" onClose={() => undefined}>
outer
</FullScreenPanel>
<FullScreenPanel isOpen={innerOpen} title="Inner" onClose={() => undefined}>
inner
</FullScreenPanel>
</>
);
describe("FullScreenPanel body scroll locking", () => {
afterEach(() => {
document.body.style.overflow = "";
});
it("keeps the body locked when a nested panel closes", () => {
document.body.style.overflow = "clip";
const view = render(<Panels innerOpen />);
expect(document.body.style.overflow).toBe("hidden");
view.rerender(<Panels innerOpen={false} />);
expect(document.body.style.overflow).toBe("hidden");
view.unmount();
expect(document.body.style.overflow).toBe("clip");
});
});
@@ -0,0 +1,178 @@
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { parse as parseToml } from "smol-toml";
import { describe, expect, it, vi } from "vitest";
import { GrokBuildProviderForm } from "@/components/providers/forms/GrokBuildProviderForm";
vi.mock("@/components/JsonEditor", () => ({
default: ({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) => (
<textarea
aria-label="raw-config"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
),
}));
describe("GrokBuildProviderForm", () => {
it("offers Codex-compatible provider presets and applies one", async () => {
const user = userEvent.setup();
const { container } = render(
<GrokBuildProviderForm
submitLabel="Save"
onSubmit={() => {}}
onCancel={() => {}}
/>,
);
await user.click(screen.getByRole("button", { name: /PatewayAI/ }));
const baseUrlInput =
container.querySelector<HTMLInputElement>("#codexBaseUrl");
const nameInput =
container.querySelector<HTMLInputElement>('input[name="name"]');
expect(baseUrlInput?.value).toBe("https://api.pateway.ai/v1");
expect(nameInput?.value).toBe("PatewayAI");
});
it("submits a complete config.toml payload with Grok defaults", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
const { container } = render(
<GrokBuildProviderForm
submitLabel="Save"
onSubmit={onSubmit}
onCancel={() => {}}
/>,
);
const nameInput =
container.querySelector<HTMLInputElement>('input[name="name"]');
const baseUrlInput =
container.querySelector<HTMLInputElement>("#codexBaseUrl");
expect(nameInput).not.toBeNull();
expect(baseUrlInput).not.toBeNull();
fireEvent.change(nameInput!, { target: { value: "Example Relay" } });
fireEvent.change(baseUrlInput!, {
target: { value: "https://relay.example.com/v1" },
});
fireEvent.change(screen.getByLabelText("API Key"), {
target: { value: "secret-key" },
});
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
const submitted = onSubmit.mock.calls[0][0];
expect(submitted.icon).toBe("");
const settings = JSON.parse(submitted.settingsConfig);
const config = parseToml(settings.config) as any;
expect(config.models.default).toBe("grok-4.5");
expect(config.model["grok-4.5"]).toEqual({
model: "grok-4.5",
base_url: "https://relay.example.com/v1",
name: "Example Relay",
api_key: "secret-key",
api_backend: "responses",
context_window: 500000,
});
});
it("maps Chat Completions presets into Grok api_backend", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(
<GrokBuildProviderForm
submitLabel="Save"
onSubmit={onSubmit}
onCancel={() => {}}
/>,
);
await user.click(screen.getByRole("button", { name: /BytePlus/ }));
await user.type(screen.getByLabelText("API Key"), "secret-key");
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
const submitted = onSubmit.mock.calls[0][0];
const settings = JSON.parse(submitted.settingsConfig);
const config = parseToml(settings.config) as any;
expect(submitted.meta.apiFormat).toBe("openai_chat");
expect(config.model[config.models.default].api_backend).toBe(
"chat_completions",
);
});
it("renders localized validation feedback for malformed TOML", async () => {
const onSubmit = vi.fn();
render(
<GrokBuildProviderForm
submitLabel="Save"
onSubmit={onSubmit}
onCancel={() => {}}
/>,
);
fireEvent.change(screen.getByLabelText("raw-config"), {
target: { value: "[models" },
});
expect(screen.getByText(/Invalid config\.toml:/)).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
});
it("loads edit-mode values and does not resubmit stale custom endpoints", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
const config = `[models]
default = "existing-profile"
[model."existing-profile"]
model = "grok-upstream"
base_url = "https://existing.example.com/v1"
name = "Existing Relay"
api_key = "existing-key"
api_backend = "responses"
context_window = 250000
`;
const { container } = render(
<GrokBuildProviderForm
providerId="existing-provider"
submitLabel="Save"
onSubmit={onSubmit}
onCancel={() => {}}
initialData={{
name: "Existing Relay",
settingsConfig: { config },
meta: {
custom_endpoints: {
"https://deleted.example.com/v1": {
url: "https://deleted.example.com/v1",
addedAt: 1,
},
},
},
}}
/>,
);
expect(
container.querySelector<HTMLInputElement>("#grokbuild-profile")?.value,
).toBe("existing-profile");
expect(
container.querySelector<HTMLInputElement>("#codexBaseUrl")?.value,
).toBe("https://existing.example.com/v1");
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0].meta.custom_endpoints).toBeUndefined();
});
});
@@ -0,0 +1,86 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CodexOAuthSection } from "@/components/providers/forms/CodexOAuthSection";
import { CopilotAuthSection } from "@/components/providers/forms/CopilotAuthSection";
const authMocks = vi.hoisted(() => ({
refetchCodex: vi.fn(),
refetchCopilot: vi.fn(),
}));
const failedStatus = (refetchStatus: () => void) => ({
accounts: [],
defaultAccountId: null,
migrationError: null,
isStatusSuccess: false,
isStatusError: true,
hasAnyAccount: false,
pollingState: "idle" as const,
deviceCode: null,
error: null,
isPolling: false,
isAddingAccount: false,
isRemovingAccount: false,
isSettingDefaultAccount: false,
addAccount: vi.fn(),
removeAccount: vi.fn(),
setDefaultAccount: vi.fn(),
cancelAuth: vi.fn(),
logout: vi.fn(),
refetchStatus,
});
vi.mock("@/components/providers/forms/hooks/useCodexOauth", () => ({
useCodexOauth: () => failedStatus(authMocks.refetchCodex),
}));
vi.mock("@/components/providers/forms/hooks/useCopilotAuth", () => ({
useCopilotAuth: () => failedStatus(authMocks.refetchCopilot),
}));
describe("managed auth status failures", () => {
beforeEach(() => {
authMocks.refetchCodex.mockResolvedValue(undefined);
authMocks.refetchCopilot.mockResolvedValue(undefined);
});
it("shows a retryable error instead of an empty Codex account selector", () => {
const onAccountSelect = vi.fn();
render(
<CodexOAuthSection
mode="select"
selectedAccountId="acct-existing"
onAccountSelect={onAccountSelect}
/>,
);
expect(screen.getByRole("alert")).toHaveTextContent(
"无法加载 ChatGPT 账号状态,请重试。",
);
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
expect(onAccountSelect).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "重试" }));
expect(authMocks.refetchCodex).toHaveBeenCalledTimes(1);
});
it("shows a retryable error instead of an empty Copilot account selector", () => {
const onAccountSelect = vi.fn();
render(
<CopilotAuthSection
mode="select"
selectedAccountId="acct-existing"
onAccountSelect={onAccountSelect}
/>,
);
expect(screen.getByRole("alert")).toHaveTextContent(
"无法加载 GitHub Copilot 账号状态,请重试。",
);
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
expect(onAccountSelect).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "重试" }));
expect(authMocks.refetchCopilot).toHaveBeenCalledTimes(1);
});
});
+9
View File
@@ -245,6 +245,7 @@ describe("McpFormModal", () => {
claude: true,
codex: true,
gemini: true,
grokbuild: true,
},
});
expect(onSave).toHaveBeenCalledTimes(1);
@@ -390,6 +391,7 @@ type = "stdio"
claude: true,
codex: false,
gemini: false,
grokbuild: false,
});
expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledWith();
@@ -423,6 +425,12 @@ type = "stdio"
expect(geminiCheckbox.checked).toBe(true);
fireEvent.click(geminiCheckbox);
const grokbuildCheckbox = screen.getByLabelText(
"mcp.unifiedPanel.apps.grokbuild",
) as HTMLInputElement;
expect(grokbuildCheckbox.checked).toBe(true);
fireEvent.click(grokbuildCheckbox);
fireEvent.click(screen.getByText("common.add"));
await waitFor(() => expect(upsertMock).toHaveBeenCalledTimes(1));
@@ -432,6 +440,7 @@ type = "stdio"
claude: false,
codex: false,
gemini: false,
grokbuild: false,
opencode: false,
openclaw: false,
hermes: false,
@@ -0,0 +1,203 @@
import { fireEvent, render, screen } from "@testing-library/react";
import type { ComponentProps, PropsWithChildren } from "react";
import { useForm } from "react-hook-form";
import { describe, expect, it, vi } from "vitest";
import { OpenCodeFormFields } from "@/components/providers/forms/OpenCodeFormFields";
import { Form } from "@/components/ui/form";
type OpenCodeFormFieldsProps = ComponentProps<typeof OpenCodeFormFields>;
const FormShell = ({ children }: PropsWithChildren) => {
const form = useForm();
return <Form {...form}>{children}</Form>;
};
const renderOpenCodeForm = (
overrides: Partial<OpenCodeFormFieldsProps> = {},
) => {
const props: OpenCodeFormFieldsProps = {
npm: "@ai-sdk/openai-compatible",
onNpmChange: vi.fn(),
apiKey: "sk-test",
onApiKeyChange: vi.fn(),
category: "custom",
shouldShowApiKeyLink: false,
websiteUrl: "",
baseUrl: "https://api.example.com/v1",
onBaseUrlChange: vi.fn(),
headers: {},
onHeadersChange: vi.fn(),
models: {
"kimi-k2": {
name: "Kimi K2",
limit: { context: 1048576, output: 131072 },
},
},
onModelsChange: vi.fn(),
extraOptions: {},
onExtraOptionsChange: vi.fn(),
...overrides,
};
return {
props,
...render(
<FormShell>
<OpenCodeFormFields {...props} />
</FormShell>,
),
};
};
const expandFirstModel = () => {
fireEvent.click(screen.getByRole("button", { name: "Toggle model details" }));
};
describe("OpenCodeFormFields", () => {
it("surfaces existing provider headers", () => {
renderOpenCodeForm({
headers: {
"HTTP-Referer": "https://cc-switch.app",
"X-Title": "CC Switch",
},
});
expect(screen.getByDisplayValue("HTTP-Referer")).toBeInTheDocument();
expect(
screen.getByDisplayValue("https://cc-switch.app"),
).toBeInTheDocument();
expect(screen.getByDisplayValue("X-Title")).toBeInTheDocument();
expect(screen.getByDisplayValue("CC Switch")).toBeInTheDocument();
});
it("updates provider headers", () => {
const onHeadersChange = vi.fn();
renderOpenCodeForm({
headers: { "X-Title": "CC Switch" },
onHeadersChange,
});
fireEvent.change(screen.getByDisplayValue("CC Switch"), {
target: { value: "OpenCode" },
});
expect(onHeadersChange).toHaveBeenCalledWith({
"X-Title": "OpenCode",
});
});
it("shows a blank header name for newly added headers", () => {
const onHeadersChange = vi.fn();
const { rerender, props } = renderOpenCodeForm({ onHeadersChange });
fireEvent.click(screen.getByRole("button", { name: "Add header" }));
const nextHeaders = onHeadersChange.mock.calls[0][0];
const headerKey = Object.keys(nextHeaders)[0];
expect(headerKey).toMatch(/^draft-header:/);
rerender(
<FormShell>
<OpenCodeFormFields {...props} headers={nextHeaders} />
</FormShell>,
);
expect(screen.getByPlaceholderText("X-Title")).toHaveValue("");
});
it("removes provider headers", () => {
const onHeadersChange = vi.fn();
renderOpenCodeForm({
headers: { "X-Title": "CC Switch" },
onHeadersChange,
});
fireEvent.click(screen.getByRole("button", { name: "Remove header" }));
expect(onHeadersChange).toHaveBeenCalledWith({});
});
it("rejects case-insensitive duplicate header names and restores the input", () => {
const onHeadersChange = vi.fn();
renderOpenCodeForm({
headers: { "X-A": "A", "X-B": "B" },
onHeadersChange,
});
const keyInput = screen.getByDisplayValue("X-B");
fireEvent.change(keyInput, { target: { value: "x-a" } });
fireEvent.blur(keyInput);
expect(onHeadersChange).not.toHaveBeenCalled();
expect(keyInput).toHaveValue("X-B");
});
it("surfaces provider options whose names start with option-", () => {
renderOpenCodeForm({
extraOptions: { "option-mode": "legacy" },
});
expect(screen.getByDisplayValue("option-mode")).toBeInTheDocument();
expect(screen.getByDisplayValue("legacy")).toBeInTheDocument();
});
it("surfaces existing model token limits", () => {
renderOpenCodeForm();
expandFirstModel();
expect(screen.getByLabelText("Context")).toHaveValue(1048576);
expect(screen.getByLabelText("Output")).toHaveValue(131072);
});
it("updates model token limits as structured numbers", () => {
const onModelsChange = vi.fn();
renderOpenCodeForm({ onModelsChange });
expandFirstModel();
fireEvent.change(screen.getByLabelText("Context"), {
target: { value: "2000000" },
});
expect(onModelsChange).toHaveBeenCalledWith({
"kimi-k2": {
name: "Kimi K2",
limit: { context: 2000000, output: 131072 },
},
});
});
it("removes model limit when both fields are cleared", () => {
const onModelsChange = vi.fn();
const { rerender, props } = renderOpenCodeForm({ onModelsChange });
expandFirstModel();
fireEvent.change(screen.getByLabelText("Context"), {
target: { value: "" },
});
const withoutContext = {
"kimi-k2": {
name: "Kimi K2",
limit: { output: 131072 },
},
};
expect(onModelsChange).toHaveBeenLastCalledWith(withoutContext);
rerender(
<FormShell>
<OpenCodeFormFields {...props} models={withoutContext} />
</FormShell>,
);
fireEvent.change(screen.getByLabelText("Output"), {
target: { value: "" },
});
expect(onModelsChange).toHaveBeenLastCalledWith({
"kimi-k2": {
name: "Kimi K2",
},
});
});
});
@@ -0,0 +1,150 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi } from "vitest";
import {
ProviderForm,
type ProviderFormValues,
} from "@/components/providers/forms/ProviderForm";
import { createTestQueryClient } from "../utils/testQueryClient";
vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
CodexOAuthSection: ({
onAccountSelect,
}: {
onAccountSelect?: (accountId: string | null) => void;
}) => (
<div>
<button type="button" onClick={() => onAccountSelect?.("acct-managed")}>
select-managed-account
</button>
<button type="button" onClick={() => onAccountSelect?.(null)}>
select-native-login
</button>
</div>
),
}));
vi.mock("@/components/providers/forms/CodexConfigEditor", () => ({
default: () => <div data-testid="codex-config-editor" />,
}));
vi.mock("@/components/providers/forms/ProviderAdvancedConfig", () => ({
ProviderAdvancedConfig: () => <div data-testid="advanced-config" />,
}));
vi.mock("@/components/providers/forms/hooks", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/components/providers/forms/hooks")>();
return {
...actual,
useCopilotAuth: () => ({
isAuthenticated: false,
isStatusSuccess: true,
isStatusError: false,
}),
useCodexOauth: () => ({
isAuthenticated: true,
isStatusSuccess: true,
isStatusError: false,
}),
useCodexCommonConfig: () => ({
useCommonConfig: false,
commonConfigSnippet: "",
commonConfigError: null,
handleCommonConfigToggle: vi.fn(),
handleCommonConfigSnippetChange: vi.fn(),
isExtracting: false,
handleExtract: vi.fn(),
clearCommonConfigError: vi.fn(),
}),
useGeminiCommonConfig: () => ({
useCommonConfig: false,
commonConfigSnippet: "",
commonConfigError: null,
handleCommonConfigToggle: vi.fn(),
handleCommonConfigSnippetChange: vi.fn(),
isExtracting: false,
handleExtract: vi.fn(),
clearCommonConfigError: vi.fn(),
}),
};
});
vi.mock("@/lib/query", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/query")>();
return {
...actual,
useSettingsQuery: () => ({
data: { commonConfigConfirmed: true },
}),
};
});
function renderCodexForm(onSubmit: (values: ProviderFormValues) => void) {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
<ProviderForm
appId="codex"
submitLabel="save-provider"
onSubmit={onSubmit}
onCancel={vi.fn()}
/>
</QueryClientProvider>,
);
}
describe("ProviderForm Codex Official managed account", () => {
it("persists the selected managed account while stripping OAuth secrets", async () => {
const onSubmit = vi.fn();
renderCodexForm(onSubmit);
fireEvent.click(screen.getByRole("button", { name: /OpenAI Official/ }));
fireEvent.click(
await screen.findByRole("button", { name: "select-managed-account" }),
);
fireEvent.click(screen.getByRole("button", { name: "save-provider" }));
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
const submitted = onSubmit.mock.calls[0][0] as ProviderFormValues;
expect(submitted).toEqual(
expect.objectContaining({
presetId: "codex-0",
presetCategory: "official",
meta: expect.objectContaining({
providerType: "codex_oauth",
authBinding: {
source: "managed_account",
authProvider: "codex_oauth",
accountId: "acct-managed",
},
}),
}),
);
expect(JSON.parse(submitted.settingsConfig)).toEqual({
auth: {},
config: "",
});
});
it("keeps Official on native browser login when no managed account is selected", async () => {
const onSubmit = vi.fn();
renderCodexForm(onSubmit);
fireEvent.click(screen.getByRole("button", { name: /OpenAI Official/ }));
fireEvent.click(
await screen.findByRole("button", { name: "select-managed-account" }),
);
fireEvent.click(
screen.getByRole("button", { name: "select-native-login" }),
);
fireEvent.click(screen.getByRole("button", { name: "save-provider" }));
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
const submitted = onSubmit.mock.calls[0][0] as ProviderFormValues;
expect(submitted.presetId).toBe("codex-0");
expect(submitted.presetCategory).toBe("official");
expect(submitted.meta?.providerType).toBeUndefined();
expect(submitted.meta?.authBinding).toBeUndefined();
});
});
+17 -4
View File
@@ -51,8 +51,8 @@ vi.mock("@/hooks/useSkills", () => ({
{
directory: "shared-skill",
name: "Shared Skill",
description: "Imported from Claude",
foundIn: ["claude"],
description: "Imported from Grok Build",
foundIn: ["grokbuild"],
path: "/tmp/shared-skill",
},
],
@@ -82,8 +82,8 @@ describe("UnifiedSkillsPanel", () => {
{
directory: "shared-skill",
name: "Shared Skill",
description: "Imported from Claude",
foundIn: ["claude"],
description: "Imported from Grok Build",
foundIn: ["grokbuild"],
path: "/tmp/shared-skill",
},
],
@@ -116,5 +116,18 @@ describe("UnifiedSkillsPanel", () => {
expect(screen.getByText("Shared Skill")).toBeInTheDocument();
expect(screen.getByText("/tmp/shared-skill")).toBeInTheDocument();
});
await act(async () => {
screen.getByText("skills.importSelected").click();
});
await waitFor(() => {
expect(importSkillsMock).toHaveBeenCalledWith([
{
directory: "shared-skill",
apps: expect.objectContaining({ grokbuild: true }),
},
]);
});
});
});
+155
View File
@@ -0,0 +1,155 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import type { ComponentProps } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { UsageDashboard } from "@/components/usage/UsageDashboard";
const useProviderStatsMock = vi.hoisted(() => vi.fn());
const useModelStatsMock = vi.hoisted(() => vi.fn());
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, fallback?: string) => fallback ?? key,
i18n: {
resolvedLanguage: "en",
language: "en",
},
}),
}));
vi.mock("framer-motion", () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}));
vi.mock("@/hooks/useUsageEventBridge", () => ({
useUsageEventBridge: () => {},
}));
vi.mock("@/lib/query/usage", async () => {
const actual =
await vi.importActual<typeof import("@/lib/query/usage")>(
"@/lib/query/usage",
);
return {
...actual,
useProviderStats: (...args: unknown[]) => useProviderStatsMock(...args),
useModelStats: (...args: unknown[]) => useModelStatsMock(...args),
};
});
vi.mock("@/components/usage/UsageHero", () => ({
UsageHero: () => <div data-testid="usage-hero" />,
}));
vi.mock("@/components/usage/UsageTrendChart", () => ({
UsageTrendChart: () => <div data-testid="usage-trend" />,
}));
vi.mock("@/components/usage/RequestLogTable", () => ({
RequestLogTable: () => <div data-testid="request-log-table" />,
}));
vi.mock("@/components/usage/ProviderStatsTable", () => ({
ProviderStatsTable: () => <div data-testid="provider-stats-table" />,
}));
vi.mock("@/components/usage/ModelStatsTable", () => ({
ModelStatsTable: () => <div data-testid="model-stats-table" />,
}));
vi.mock("@/components/usage/PricingConfigPanel", () => ({
PricingConfigPanel: () => <div data-testid="pricing-config-panel" />,
}));
vi.mock("@/components/usage/UsageDateRangePicker", () => ({
UsageDateRangePicker: () => <button type="button">date-range</button>,
}));
vi.mock("@/components/ui/select", () => ({
Select: ({ value, onValueChange, children }: any) => (
<div data-testid={`select-${value}`}>
{children}
<button type="button" onClick={() => onValueChange?.("5000")}>
choose-5000
</button>
</div>
),
SelectTrigger: ({ children, ...props }: any) => (
<button type="button" {...props}>
{children}
</button>
),
SelectValue: () => null,
SelectContent: ({ children }: any) => <div>{children}</div>,
SelectItem: ({ children, ...props }: any) => <div {...props}>{children}</div>,
}));
const renderDashboard = (props: ComponentProps<typeof UsageDashboard> = {}) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<UsageDashboard {...props} />
</QueryClientProvider>,
);
};
describe("UsageDashboard", () => {
beforeEach(() => {
useProviderStatsMock.mockReset();
useModelStatsMock.mockReset();
useProviderStatsMock.mockReturnValue({ data: [] });
useModelStatsMock.mockReturnValue({ data: [] });
});
it("uses the saved refresh interval when mounted", () => {
renderDashboard({ refreshIntervalMs: 5000 });
expect(screen.getByTestId("select-5000")).toBeInTheDocument();
});
it("persists refresh interval changes", async () => {
const onRefreshIntervalChange = vi.fn().mockResolvedValue(true);
renderDashboard({ onRefreshIntervalChange });
fireEvent.click(
within(screen.getByTestId("select-30000")).getByRole("button", {
name: "choose-5000",
}),
);
await waitFor(() =>
expect(onRefreshIntervalChange).toHaveBeenCalledWith(5000),
);
expect(screen.getByTestId("select-5000")).toBeInTheDocument();
});
it("rolls back optimistic interval changes when persistence fails", async () => {
const onRefreshIntervalChange = vi.fn().mockResolvedValue(false);
renderDashboard({ onRefreshIntervalChange });
fireEvent.click(
within(screen.getByTestId("select-30000")).getByRole("button", {
name: "choose-5000",
}),
);
await waitFor(() =>
expect(onRefreshIntervalChange).toHaveBeenCalledWith(5000),
);
await waitFor(() =>
expect(screen.getByTestId("select-30000")).toBeInTheDocument(),
);
});
});