feat(FN-2529): merge fusion/fn-2529

This commit is contained in:
gsxdsm
2026-04-25 17:44:55 -07:00
parent 9a22ae9be4
commit 833b9dac61
9 changed files with 231 additions and 56 deletions

View File

@@ -1627,7 +1627,11 @@ export function ModelOnboardingModal({
data-testid={`onboarding-provider-card-${provider.id}`}
className={`onboarding-provider-card${provider.authenticated ? " onboarding-provider-card--connected" : ""}`}
>
<div className="onboarding-provider-card__icon">
<div
className="onboarding-provider-card__icon"
data-testid={`onboarding-provider-icon-${provider.id}`}
aria-hidden="true"
>
<ProviderIcon provider={provider.id} size="md" />
</div>
<div className="onboarding-provider-card__body">
@@ -1667,7 +1671,11 @@ export function ModelOnboardingModal({
data-testid={`onboarding-provider-card-${provider.id}`}
className={`onboarding-provider-card${provider.authenticated ? " onboarding-provider-card--connected" : ""}`}
>
<div className="onboarding-provider-card__icon">
<div
className="onboarding-provider-card__icon"
data-testid={`onboarding-provider-icon-${provider.id}`}
aria-hidden="true"
>
<ProviderIcon provider={provider.id} size="md" />
</div>
<div className="onboarding-provider-card__body">

View File

@@ -155,6 +155,46 @@ function KimiIcon({ size, color, label = "Kimi" }: { size: number; color: string
);
}
// OpenRouter ring mark — simplified geometric version of the OpenRouter brand symbol.
function OpenRouterIcon({ size, color, label = "OpenRouter" }: { size: number; color: string; label?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="openrouter-icon"
aria-label={label}
>
<path
d="M12 2.5a9.5 9.5 0 1 0 9.5 9.5h-2.7a6.8 6.8 0 1 1-2-4.8l-3.1 3.1H22V2.5l-3.2 3.2A9.45 9.45 0 0 0 12 2.5"
fill={color}
/>
</svg>
);
}
// GitHub logo (Octocat mark) from SimpleIcons.
function GitHubIcon({ size, color, label = "GitHub" }: { size: number; color: string; label?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="github-icon"
aria-label={label}
>
<path
d="M12 0a12 12 0 0 0-3.79 23.39c.6.11.82-.26.82-.58v-2.23c-3.34.73-4.04-1.41-4.04-1.41-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.74.08-.74 1.2.08 1.84 1.24 1.84 1.24 1.08 1.84 2.82 1.31 3.5 1 .1-.78.42-1.31.77-1.62-2.67-.3-5.47-1.34-5.47-5.95 0-1.31.47-2.38 1.24-3.22-.12-.31-.54-1.56.12-3.24 0 0 1.01-.32 3.3 1.23a11.3 11.3 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.68.24 2.93.12 3.24.77.84 1.24 1.91 1.24 3.22 0 4.62-2.8 5.64-5.48 5.94.43.37.82 1.1.82 2.22v3.29c0 .32.22.69.83.58A12 12 0 0 0 12 0"
fill={color}
/>
</svg>
);
}
// Anthropic "A" mark composited with a small terminal "> _" badge in the
// bottom-right, visually signalling "Anthropic, but via the local CLI".
function ClaudeCliIcon({ size, color, label = "Anthropic — via Claude CLI" }: { size: number; color: string; label?: string }) {
@@ -179,7 +219,7 @@ function ClaudeCliIcon({ size, color, label = "Anthropic — via Claude CLI" }:
<rect x="13" y="13" width="10" height="9" rx="1.5" fill={color} />
<path
d="M15.2 16.2l1.6 1.4-1.6 1.4M18.6 19.6h2.4"
stroke="var(--bg-primary, #111)"
stroke="var(--provider-icon-contrast)"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
@@ -193,18 +233,30 @@ const providerConfig: Record<
string,
{ component: typeof AnthropicIcon; color: string; label?: string }
> = {
anthropic: { component: AnthropicIcon, color: "#d4a27f" }, // warm tan
"claude-cli": { component: ClaudeCliIcon, color: "#d4a27f", label: "Anthropic — via Claude CLI" },
openai: { component: OpenAIIcon, color: "#10a37f" }, // green
"openai-codex": { component: OpenAIIcon, color: "#10a37f", label: "OpenAI Codex" }, // green (same as openai)
google: { component: GeminiIcon, color: "#4285f4" }, // blue
gemini: { component: GeminiIcon, color: "#4285f4" }, // blue (same as google)
ollama: { component: OllamaIcon, color: "#fff" }, // white
minimax: { component: MiniMaxIcon, color: "#E73562" }, // pink/red
zai: { component: ZaiIcon, color: "#1A6DFF" }, // blue
kimi: { component: KimiIcon, color: "#6C5CE7" }, // purple
moonshot: { component: KimiIcon, color: "#6C5CE7" }, // purple (same as kimi)
"kimi-coding": { component: KimiIcon, color: "#6C5CE7", label: "Kimi" }, // purple (Kimi alias)
// Branded provider colors are tokenized in app/styles.css for theme-system consistency.
anthropic: { component: AnthropicIcon, color: "var(--provider-anthropic)" },
"claude-cli": { component: ClaudeCliIcon, color: "var(--provider-anthropic)", label: "Anthropic — via Claude CLI" },
openai: { component: OpenAIIcon, color: "var(--provider-openai)" },
"openai-codex": { component: OpenAIIcon, color: "var(--provider-openai)", label: "OpenAI Codex" }, // OpenAI alias
google: { component: GeminiIcon, color: "var(--provider-gemini)" },
gemini: { component: GeminiIcon, color: "var(--provider-gemini)" }, // Gemini alias family
// Monochrome marks use theme-aware text color for dark/light safety.
ollama: { component: OllamaIcon, color: "var(--text)" },
github: { component: GitHubIcon, color: "var(--text)" },
"github-copilot": { component: GitHubIcon, color: "var(--text)", label: "GitHub Copilot" },
// OpenRouter appears in onboarding auth cards and should not fall back to CPU.
openrouter: { component: OpenRouterIcon, color: "var(--provider-openrouter)" },
minimax: { component: MiniMaxIcon, color: "var(--provider-minimax)" },
zai: { component: ZaiIcon, color: "var(--provider-zai)" },
kimi: { component: KimiIcon, color: "var(--provider-kimi)" },
moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias
"kimi-coding": { component: KimiIcon, color: "var(--provider-kimi)", label: "Kimi" }, // Kimi alias
};
export function ProviderIcon({ provider, size = "sm" }: ProviderIconProps) {

View File

@@ -523,6 +523,17 @@
font-weight: 500;
color: var(--text);
}
.auth-provider-icon-slot {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: calc(var(--space-md) + var(--space-xs) * 2);
block-size: calc(var(--space-md) + var(--space-xs) * 2);
flex-shrink: 0;
}
.auth-provider-icon-slot .provider-icon {
display: inline-flex;
}
.auth-hint {
display: block;
padding: 12px 4px 0;

View File

@@ -17,6 +17,7 @@ const PiExtensionsManager = lazy(() => import("./PiExtensionsManager").then((m)
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
import { PluginSlot } from "./PluginSlot";
import { AgentPromptsManager } from "./AgentPromptsManager";
import { ProviderIcon } from "./ProviderIcon";
import { applyPresetToSelection, generateUniquePresetId } from "../utils/modelPresets";
import { appendTokenQuery } from "../auth";
import "./SettingsModal.css";
@@ -3441,6 +3442,14 @@ export function SettingsModal({
<div key={provider.id} className="auth-provider-card auth-provider-card--authenticated">
<div className="auth-provider-header">
<div className="auth-provider-info">
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
<span
className="auth-provider-icon-slot"
data-testid={`auth-provider-icon-${provider.id}`}
aria-hidden="true"
>
<ProviderIcon provider={provider.id} size="md" />
</span>
<strong>{provider.name}</strong>
<span
data-testid={`auth-status-${provider.id}`}
@@ -3516,6 +3525,14 @@ export function SettingsModal({
<div key={provider.id} className="auth-provider-card">
<div className="auth-provider-header">
<div className="auth-provider-info">
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
<span
className="auth-provider-icon-slot"
data-testid={`auth-provider-icon-${provider.id}`}
aria-hidden="true"
>
<ProviderIcon provider={provider.id} size="md" />
</span>
<strong>{provider.name}</strong>
<span
data-testid={`auth-status-${provider.id}`}

View File

@@ -34,10 +34,10 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
expect(openaiIcon).toBeInTheDocument();
expect(openaiIcon).toHaveAttribute("aria-label", "OpenAI");
// Verify the SVG has the correct fill color (#10a37f - OpenAI green)
// Verify the SVG has the correct fill color (var(--provider-openai) - OpenAI green)
const paths = openaiIcon.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
});
it("renders actual OpenAI SVG icon in dropdown group header", async () => {
@@ -58,7 +58,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
// Verify the icon has the correct color
const paths = openaiIcon.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
});
it("renders actual provider icons for all providers in dropdown", async () => {
@@ -71,17 +71,18 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
const anthropicIcon = screen.getByTestId("anthropic-icon");
expect(anthropicIcon).toBeInTheDocument();
expect(anthropicIcon).toHaveAttribute("aria-label", "Anthropic");
expect(anthropicIcon.querySelector("path")).toHaveAttribute("fill", "#d4a27f");
expect(anthropicIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-anthropic)");
const openaiIcon = screen.getByTestId("openai-icon");
expect(openaiIcon).toBeInTheDocument();
expect(openaiIcon).toHaveAttribute("aria-label", "OpenAI");
expect(openaiIcon.querySelector("path")).toHaveAttribute("fill", "#10a37f");
expect(openaiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-openai)");
const ollamaIcon = screen.getByTestId("ollama-icon");
expect(ollamaIcon).toBeInTheDocument();
expect(ollamaIcon).toHaveAttribute("aria-label", "Ollama");
expect(ollamaIcon.querySelector("path")).toHaveAttribute("fill", "#fff");
// Ollama icon is intentionally theme-aware (token-based) rather than hardcoded white.
expect(ollamaIcon.querySelector("path")).toHaveAttribute("fill", "var(--text)");
});
it("renders both trigger icon and group header icons when dropdown is open with OpenAI selected", async () => {
@@ -101,7 +102,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
// Both should have correct attributes
openaiIcons.forEach((icon) => {
expect(icon).toHaveAttribute("aria-label", "OpenAI");
expect(icon.querySelector("path")).toHaveAttribute("fill", "#10a37f");
expect(icon.querySelector("path")).toHaveAttribute("fill", "var(--provider-openai)");
});
});
@@ -135,7 +136,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
// Verify the first one has correct attributes
const kimiIcon = kimiIcons[0];
expect(kimiIcon).toHaveAttribute("aria-label", "Kimi");
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "#6C5CE7");
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-kimi)");
});
it("uses explicit icon/text layout hooks for favorited model rows", async () => {
@@ -178,7 +179,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
const kimiIcon = screen.getByTestId("kimi-icon");
expect(kimiIcon).toBeInTheDocument();
expect(kimiIcon).toHaveAttribute("aria-label", "Kimi");
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "#6C5CE7");
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-kimi)");
});
it("renders Kimi icon in trigger when moonshot model is selected (alias)", () => {
@@ -187,6 +188,6 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
const kimiIcon = screen.getByTestId("kimi-icon");
expect(kimiIcon).toBeInTheDocument();
expect(kimiIcon).toHaveAttribute("aria-label", "Kimi");
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "#6C5CE7");
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-kimi)");
});
});

View File

@@ -499,6 +499,20 @@ describe("ModelOnboardingModal", () => {
expect(screen.queryByTestId("onboarding-advanced-provider-settings")).toBeNull();
});
it("renders onboarding provider icon wrapper for connected advanced providers", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
{ id: "openrouter", name: "OpenRouter", authenticated: true, type: "api_key" },
],
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
const openRouterWrapper = await screen.findByTestId("onboarding-provider-icon-openrouter");
expect(within(openRouterWrapper).getByTestId("provider-icon")).toHaveAttribute("data-provider", "openrouter");
});
it("shows model dropdown in AI Setup step", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
@@ -754,7 +768,7 @@ describe("ModelOnboardingModal", () => {
expect(description.closest(".onboarding-provider-card")).toBeTruthy();
});
it("renders ProviderIcon for each provider card", async () => {
it("renders stable onboarding-provider-icon wrappers for provider cards", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await waitFor(() => {
@@ -762,15 +776,11 @@ describe("ModelOnboardingModal", () => {
expect(screen.getByText("OpenAI")).toBeTruthy();
});
// Verify provider icons are rendered for both providers
const icons = screen.getAllByTestId("provider-icon");
expect(icons.length).toBe(2);
const anthropicIconWrapper = screen.getByTestId("onboarding-provider-icon-anthropic");
const openaiIconWrapper = screen.getByTestId("onboarding-provider-icon-openai");
// Verify the data-provider attributes match expected IDs
const anthropicIcon = icons.find((icon) => icon.getAttribute("data-provider") === "anthropic");
const openaiIcon = icons.find((icon) => icon.getAttribute("data-provider") === "openai");
expect(anthropicIcon).toBeTruthy();
expect(openaiIcon).toBeTruthy();
expect(within(anthropicIconWrapper).getByTestId("provider-icon")).toHaveAttribute("data-provider", "anthropic");
expect(within(openaiIconWrapper).getByTestId("provider-icon")).toHaveAttribute("data-provider", "openai");
});
it("applies connected modifier class to authenticated provider cards", async () => {

View File

@@ -12,7 +12,7 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for openai-codex", () => {
render(<ProviderIcon provider="openai-codex" />);
const icon = screen.getByTestId("openai-icon").parentElement;
expect(icon).toHaveStyle({ color: "#10a37f" });
expect(icon).toHaveStyle({ color: "var(--provider-openai)" });
});
it("passes correct color to SVG fill for openai-codex", () => {
@@ -20,7 +20,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("openai-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
});
it("renders Anthropic brand icon for anthropic provider", () => {
@@ -29,6 +29,15 @@ describe("ProviderIcon", () => {
expect(screen.getByLabelText("Anthropic")).toBeInTheDocument();
});
it("renders claude-cli icon with tokenized contrast stroke", () => {
render(<ProviderIcon provider="claude-cli" />);
const svg = screen.getByTestId("claude-cli-icon");
expect(svg).toBeInTheDocument();
expect(screen.getByLabelText("Anthropic — via Claude CLI")).toBeInTheDocument();
const badgeGlyph = svg.querySelector('path[stroke]');
expect(badgeGlyph).toHaveAttribute("stroke", "var(--provider-icon-contrast)");
});
it("renders OpenAI brand icon for openai provider", () => {
render(<ProviderIcon provider="openai" />);
expect(screen.getByTestId("openai-icon")).toBeInTheDocument();
@@ -82,25 +91,25 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for anthropic", () => {
render(<ProviderIcon provider="anthropic" />);
const icon = screen.getByTestId("anthropic-icon").parentElement;
expect(icon).toHaveStyle({ color: "#d4a27f" });
expect(icon).toHaveStyle({ color: "var(--provider-anthropic)" });
});
it("applies provider-specific color for openai", () => {
render(<ProviderIcon provider="openai" />);
const icon = screen.getByTestId("openai-icon").parentElement;
expect(icon).toHaveStyle({ color: "#10a37f" });
expect(icon).toHaveStyle({ color: "var(--provider-openai)" });
});
it("applies provider-specific color for google", () => {
render(<ProviderIcon provider="google" />);
const icon = screen.getByTestId("gemini-icon").parentElement;
expect(icon).toHaveStyle({ color: "#4285f4" });
expect(icon).toHaveStyle({ color: "var(--provider-gemini)" });
});
it("applies provider-specific color for ollama", () => {
it("applies theme-safe color for ollama", () => {
render(<ProviderIcon provider="ollama" />);
const icon = screen.getByTestId("ollama-icon").parentElement;
expect(icon).toHaveStyle({ color: "#fff" });
expect(icon).toHaveStyle({ color: "var(--text)" });
});
it("applies default color for unknown providers", () => {
@@ -156,7 +165,7 @@ describe("ProviderIcon", () => {
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
// First path should have the provider color
expect(paths[0]).toHaveAttribute("fill", "#d4a27f");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-anthropic)");
});
it("passes correct color to SVG fill for openai", () => {
@@ -164,7 +173,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("openai-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
});
it("passes correct color to SVG fill for gemini", () => {
@@ -172,15 +181,15 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("gemini-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#4285f4");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-gemini)");
});
it("passes correct color to SVG fill for ollama", () => {
it("passes theme-safe color to SVG fill for ollama", () => {
render(<ProviderIcon provider="ollama" />);
const svg = screen.getByTestId("ollama-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#fff");
expect(paths[0]).toHaveAttribute("fill", "var(--text)");
});
it("renders MiniMax brand icon for minimax provider", () => {
@@ -192,7 +201,7 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for minimax", () => {
render(<ProviderIcon provider="minimax" />);
const icon = screen.getByTestId("minimax-icon").parentElement;
expect(icon).toHaveStyle({ color: "#E73562" });
expect(icon).toHaveStyle({ color: "var(--provider-minimax)" });
});
it("passes correct color to SVG fill for minimax", () => {
@@ -200,7 +209,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("minimax-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#E73562");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-minimax)");
});
it("normalizes Minimax (capitalized) to minimax", () => {
@@ -219,7 +228,7 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for zai", () => {
render(<ProviderIcon provider="zai" />);
const icon = screen.getByTestId("zai-icon").parentElement;
expect(icon).toHaveStyle({ color: "#1A6DFF" });
expect(icon).toHaveStyle({ color: "var(--provider-zai)" });
});
it("passes correct color to SVG fill for zai", () => {
@@ -227,7 +236,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("zai-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#1A6DFF");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-zai)");
});
it("normalizes Zai (capitalized) to zai", () => {
@@ -246,7 +255,7 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for kimi", () => {
render(<ProviderIcon provider="kimi" />);
const icon = screen.getByTestId("kimi-icon").parentElement;
expect(icon).toHaveStyle({ color: "#6C5CE7" });
expect(icon).toHaveStyle({ color: "var(--provider-kimi)" });
});
it("passes correct color to SVG fill for kimi", () => {
@@ -254,7 +263,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("kimi-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#6C5CE7");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
});
it("normalizes Kimi (capitalized) to kimi", () => {
@@ -273,7 +282,7 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for moonshot (alias)", () => {
render(<ProviderIcon provider="moonshot" />);
const icon = screen.getByTestId("kimi-icon").parentElement;
expect(icon).toHaveStyle({ color: "#6C5CE7" });
expect(icon).toHaveStyle({ color: "var(--provider-kimi)" });
});
it("passes correct color to SVG fill for moonshot (alias)", () => {
@@ -281,7 +290,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("kimi-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#6C5CE7");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
});
it("normalizes Moonshot (capitalized) to moonshot", () => {
@@ -291,6 +300,24 @@ describe("ProviderIcon", () => {
expect(wrapper).toHaveAttribute("data-provider", "moonshot");
});
it("renders OpenRouter brand icon for openrouter provider", () => {
render(<ProviderIcon provider="openrouter" />);
expect(screen.getByTestId("openrouter-icon")).toBeInTheDocument();
expect(screen.getByLabelText("OpenRouter")).toBeInTheDocument();
});
it("renders GitHub brand icon for github provider", () => {
render(<ProviderIcon provider="github" />);
expect(screen.getByTestId("github-icon")).toBeInTheDocument();
expect(screen.getByLabelText("GitHub")).toBeInTheDocument();
});
it("reuses GitHub icon for github-copilot alias", () => {
render(<ProviderIcon provider="github-copilot" />);
expect(screen.getByTestId("github-icon")).toBeInTheDocument();
expect(screen.getByLabelText("GitHub Copilot")).toBeInTheDocument();
});
it("renders Kimi brand icon for kimi-coding provider (alias)", () => {
render(<ProviderIcon provider="kimi-coding" />);
expect(screen.getByTestId("kimi-icon")).toBeInTheDocument();
@@ -300,7 +327,7 @@ describe("ProviderIcon", () => {
it("applies provider-specific color for kimi-coding (alias)", () => {
render(<ProviderIcon provider="kimi-coding" />);
const icon = screen.getByTestId("kimi-icon").parentElement;
expect(icon).toHaveStyle({ color: "#6C5CE7" });
expect(icon).toHaveStyle({ color: "var(--provider-kimi)" });
});
it("passes correct color to SVG fill for kimi-coding (alias)", () => {
@@ -308,7 +335,7 @@ describe("ProviderIcon", () => {
const svg = screen.getByTestId("kimi-icon");
const paths = svg.querySelectorAll("path");
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toHaveAttribute("fill", "#6C5CE7");
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
});
// Regression test: verify the Kimi icon is the crescent moon shape, not the old "K" placeholder

View File

@@ -385,6 +385,45 @@ describe("SettingsModal", () => {
});
});
describe("Authentication provider icon wrappers", () => {
it("renders stable auth-provider-icon wrappers with branded and fallback SVG behavior", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "openrouter", name: "OpenRouter", authenticated: true, type: "api_key" },
{ id: "unknown-provider", name: "Unknown Provider", authenticated: false, type: "api_key" },
],
});
renderModal();
await waitForSettingsModalReady();
const openRouterIconWrapper = await screen.findByTestId("auth-provider-icon-openrouter");
expect(within(openRouterIconWrapper).getByTestId("openrouter-icon")).toBeInTheDocument();
const unknownIconWrapper = screen.getByTestId("auth-provider-icon-unknown-provider");
const fallbackSvg = unknownIconWrapper.querySelector("svg");
expect(fallbackSvg).toBeInTheDocument();
expect(fallbackSvg).not.toHaveAttribute("data-testid");
});
it("renders icon wrappers for both authenticated and available provider rows", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" },
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
],
});
renderModal();
await waitForSettingsModalReady();
expect(screen.getByTestId("auth-provider-icon-github")).toBeInTheDocument();
expect(screen.getByTestId("auth-provider-icon-openai")).toBeInTheDocument();
expect(screen.getByTestId("auth-status-github")).toHaveTextContent("✓ Active");
expect(screen.getByTestId("auth-status-openai")).toHaveTextContent("✗ Not connected");
});
});
describe("Plugins section navigation", () => {
it("does not render a standalone Pi Extensions sidebar item", async () => {
renderModal();

View File

@@ -161,6 +161,16 @@
/* Logo & branding tokens */
--logo-accent: var(--todo);
/* Provider brand tokens used by ProviderIcon in settings/onboarding auth flows. */
--provider-anthropic: #d4a27f;
--provider-openai: #10a37f;
--provider-gemini: #4285f4;
--provider-openrouter: #6f4cff;
--provider-minimax: #e73562;
--provider-zai: #1a6dff;
--provider-kimi: #6c5ce7;
--provider-icon-contrast: var(--bg);
/* Task-creation CTA tokens */
--cta-bg: #238636;
--cta-border: #2ea043;