Files
fusion/packages/dashboard/app/components/__tests__/CustomModelDropdown.credential-instance.test.tsx
gsxdsm 0e8f6769ba FN-8666: add credential instance pickers
Add credential-instance selection across dashboard model configuration surfaces.

- Expose provider credential instances in custom model dropdowns and task, workflow, and settings forms.
- Persist per-role credential-instance overrides through dashboard APIs and effective model resolution.
- Document credential-instance precedence and cover dropdown/settings behavior, including task state synchronization.

Files changed:
 .changeset/fn-8666-credential-instance-picker.md   |   7 ++
 docs/settings-reference.md                         |   2 +
 docs/workflow-steps.md                             |   2 +-
 packages/dashboard/app/api.ts                      |   1 +
 packages/dashboard/app/api/models-usage.ts         |  27 ++++-
 packages/dashboard/app/api/tasks.ts                |   8 ++
 .../app/components/CustomModelDropdown.css         |  12 ++-
 .../app/components/CustomModelDropdown.tsx         |  71 ++++++++++++-
 .../dashboard/app/components/InlineCreateCard.tsx  |  23 +++-
 packages/dashboard/app/components/ListView.tsx     |  29 +++++-
 .../app/components/ModelSelectionModal.tsx         |  25 ++++-
 .../dashboard/app/components/ModelSelectorTab.tsx  |  29 +++++-
 packages/dashboard/app/components/NewTaskModal.tsx |  23 +++-
 .../dashboard/app/components/QuickEntryBox.tsx     |  28 +++++
 .../dashboard/app/components/SettingsModal.tsx     |   2 +
 .../dashboard/app/components/TaskDetailModal.tsx   |  32 +++++-
 packages/dashboard/app/components/TaskForm.tsx     |  18 ++++
 .../app/components/WorkflowNodeEditor.tsx          |  13 ++-
 .../app/components/WorkflowSettingsPanel.tsx       |  38 ++++++-
 ...ustomModelDropdown.credential-instance.test.tsx | 111 ++++++++++++++++++++
 .../__tests__/WorkflowSettingsPanel.test.tsx       |   9 +-
 .../app/components/effective-model-resolution.ts   |   8 +-
 .../settings/sections/GlobalModelsSection.tsx      |  31 +++++-
 .../settings/sections/ProjectModelsSection.tsx     | 116 ++++++++++++++++-----
 .../ProjectModelsSection.chatDefault.test.tsx      |  33 +++++-
 packages/dashboard/app/hooks/useFavorites.ts       |   6 +-
 packages/dashboard/app/hooks/useModelsCache.ts     |   5 +-
 .../src/routes/register-task-workflow-routes.ts    |  17 ++-
 28 files changed, 655 insertions(+), 71 deletions(-)

Fusion-Task-Id: FN-8666

Fusion-Task-Lineage: 6c3711b3-68b8-47a0-ac36-4b64846adf39

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 04:03:26 -07:00

112 lines
5.1 KiB
TypeScript

import type { ComponentProps } from "react";
import { describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CustomModelDropdown } from "../CustomModelDropdown";
vi.mock("../ProviderIcon", () => ({
ProviderIcon: () => <span />,
}));
const models = [{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 }];
async function openDropdown(props: Partial<ComponentProps<typeof CustomModelDropdown>>) {
const user = userEvent.setup();
render(<CustomModelDropdown label="Model" value="openai/gpt-4o" onChange={vi.fn()} models={models} {...props} />);
await user.click(screen.getByRole("button", { name: "Model" }));
return user;
}
describe("CustomModelDropdown credential instance", () => {
it.each([undefined, {}, { openai: { instances: [] } }, { openai: { instances: [{ id: "only", isDefault: true }] } }])(
"renders no instance control unless the selected provider has two instances",
async (credentialInstances) => {
await openDropdown({ credentialInstances });
expect(screen.queryByTestId("custom-model-dropdown-credential-instance")).toBeNull();
expect(screen.queryByTestId("custom-model-dropdown-credential-instance-badge")).toBeNull();
},
);
it("keeps hidden menus structurally free of an instance shell at desktop and mobile breakpoints", async () => {
for (const width of [1024, 768, 480]) {
Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
const user = await openDropdown({ credentialInstances: { openai: { instances: [{ id: "only", isDefault: true }] } } });
const menu = screen.getAllByTestId("model-combobox-portal").at(-1)!;
expect([...menu.children].map((element) => element.className)).not.toContain("model-combobox-instance");
expect(screen.queryByTestId("custom-model-dropdown-credential-instance")).toBeNull();
expect(screen.queryByTestId("custom-model-dropdown-credential-instance-badge")).toBeNull();
await user.keyboard("{Escape}");
cleanup();
}
});
it("shows availability-driven control even before an owner supplies a persistence callback", async () => {
await openDropdown({
credentialInstances: { openai: { instances: [{ id: "primary", isDefault: true }, { id: "backup", isDefault: false }] } },
});
expect(screen.getByTestId("custom-model-dropdown-credential-instance")).toBeDisabled();
});
it("deduplicates instances, preserves a stale selection, and emits empty only for Default", async () => {
const onCredentialInstanceChange = vi.fn();
const user = await openDropdown({
credentialInstanceId: "stale",
onCredentialInstanceChange,
credentialInstances: {
openai: { instances: [{ id: "primary", isDefault: true }, { id: "primary", isDefault: false }, { id: "backup", isDefault: false }] },
},
});
const select = screen.getByTestId("custom-model-dropdown-credential-instance") as HTMLSelectElement;
expect([...select.options].map((option) => option.value)).toEqual(["", "primary", "backup", "stale"]);
expect(select.value).toBe("stale");
await user.selectOptions(select, "backup");
expect(onCredentialInstanceChange).toHaveBeenLastCalledWith("backup");
await user.selectOptions(select, "");
expect(onCredentialInstanceChange).toHaveBeenLastCalledWith("");
});
it("recomputes availability after the selected provider changes without affecting keyboard dismissal", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const onCredentialInstanceChange = vi.fn();
const { rerender } = render(
<CustomModelDropdown
label="Model"
value="openai/gpt-4o"
onChange={onChange}
models={[
...models,
{ provider: "anthropic", id: "claude", name: "Claude", reasoning: false, contextWindow: 128000 },
]}
credentialInstances={{
openai: { instances: [{ id: "primary", isDefault: true }, { id: "backup", isDefault: false }] },
anthropic: { instances: [{ id: "only", isDefault: true }] },
}}
onCredentialInstanceChange={onCredentialInstanceChange}
/>,
);
await user.click(screen.getByRole("button", { name: "Model" }));
expect(screen.getByTestId("custom-model-dropdown-credential-instance")).toBeTruthy();
rerender(
<CustomModelDropdown
label="Model"
value="anthropic/claude"
onChange={onChange}
models={[
...models,
{ provider: "anthropic", id: "claude", name: "Claude", reasoning: false, contextWindow: 128000 },
]}
credentialInstances={{
openai: { instances: [{ id: "primary", isDefault: true }, { id: "backup", isDefault: false }] },
anthropic: { instances: [{ id: "only", isDefault: true }] },
}}
onCredentialInstanceChange={onCredentialInstanceChange}
/>,
);
expect(screen.queryByTestId("custom-model-dropdown-credential-instance")).toBeNull();
await user.keyboard("{Escape}");
expect(screen.queryByTestId("model-combobox-portal")).toBeNull();
});
});