mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(grokbuild): add first-class Grok Build support (#5453)
* feat(grokbuild): add backend integration * feat(grokbuild): add provider and management UI * test(grokbuild): cover configuration and integrations * fix(grokbuild): address backend review feedback * fix(grokbuild): complete UI review feedback * feat(grokbuild): add CLI lifecycle management * fix(grokbuild): align provider icon fallback * test(grokbuild): cover provider icon persistence
This commit is contained in:
@@ -125,4 +125,45 @@ describe("AddProviderDialog", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user