feat(FN-1196): support API-key providers in dashboard auth flow

- Wrap dashboard auth storage with API-key provider helpers derived from model registry providers
- Normalize provider display names and bridge set/clear/has API-key operations to AuthStorage credentials
- Expand route and onboarding tests for mixed OAuth/API-key states and API-key-authenticated setup paths
- Stabilize assignment-trigger heartbeat timing test by replacing fixed delays with waitFor assertions
This commit is contained in:
gsxdsm
2026-04-08 13:54:30 -07:00
parent 8c0d30d206
commit 8354a86a82
6 changed files with 242 additions and 7 deletions

View File

@@ -584,6 +584,27 @@ describe("App auto-open Settings on unauthenticated", () => {
expect(screen.queryByText("Set Up AI Provider")).toBeNull();
});
it("treats authenticated API-key providers as valid auth for onboarding checks", async () => {
(fetchAuthStatus as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
providers: [
{ id: "openrouter", name: "OpenRouter", authenticated: true, type: "api_key" },
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
],
});
(fetchGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
defaultProvider: "openrouter",
defaultModelId: "gpt-4o",
});
render(<App />);
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
await waitFor(() => expect(fetchSettings).toHaveBeenCalledTimes(1));
expect(screen.queryByText("Settings")).toBeNull();
expect(screen.queryByText("Set Up AI Provider")).toBeNull();
});
it("auto-opens onboarding when providers are authenticated but default model is missing", async () => {
(fetchAuthStatus as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
providers: [

View File

@@ -93,6 +93,18 @@ describe("ModelOnboardingModal", () => {
expect(screen.getByTestId("onboarding-apikey-save-openai")).toBeTruthy();
});
it("renders OAuth and API key providers at the same time", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Anthropic")).toBeTruthy();
expect(screen.getByText("OpenAI")).toBeTruthy();
});
expect(screen.getByText("Login")).toBeTruthy();
expect(screen.getByTestId("onboarding-apikey-save-openai")).toBeTruthy();
});
it("disables Continue button when no providers are authenticated", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
@@ -103,6 +115,30 @@ describe("ModelOnboardingModal", () => {
expect(screen.getByText("Continue →").closest("button")?.disabled).toBe(true);
});
it("supports mixed auth states across OAuth and API key providers", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
{ id: "openrouter", name: "OpenRouter", authenticated: true, type: "api_key" },
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
],
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("onboarding-auth-status-openrouter")).toBeTruthy();
expect(screen.getByTestId("onboarding-auth-status-openai")).toBeTruthy();
});
expect(screen.getByTestId("onboarding-auth-status-openrouter").textContent).toContain("Key saved");
expect(screen.getByTestId("onboarding-auth-status-openai").textContent).toContain("No API key");
await waitFor(() => {
expect(screen.getByText("Choose Default Model")).toBeTruthy();
}, { timeout: 3000 });
});
it("initiates OAuth login when Login is clicked", async () => {
const mockWindowOpen = vi.fn();
vi.spyOn(window, "open").mockImplementation(mockWindowOpen);
@@ -137,6 +173,41 @@ describe("ModelOnboardingModal", () => {
});
});
it("auto-advances to model selection after API key authentication", async () => {
mockFetchAuthStatus
.mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
],
})
.mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
{ id: "openai", name: "OpenAI", authenticated: true, type: "api_key" },
],
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("onboarding-apikey-input-openai")).toBeTruthy();
});
fireEvent.change(screen.getByTestId("onboarding-apikey-input-openai"), {
target: { value: "sk-api-key" },
});
fireEvent.click(screen.getByTestId("onboarding-apikey-save-openai"));
await waitFor(() => {
expect(mockSaveApiKey).toHaveBeenCalledWith("openai", "sk-api-key");
});
await waitFor(() => {
expect(screen.getByText("Choose Default Model")).toBeTruthy();
}, { timeout: 3000 });
});
it("shows Save button as disabled when API key input is empty", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);