feat(FN-834): add per-workflow-step model override UI and support
- Add model provider/model selection fields to WorkflowStepManager component - Add dropdowns for choosing executor model per workflow step definition - Wire executor to use per-step model overrides when configured - Add comprehensive tests for WorkflowStepManager (340 lines) - Add CSS styles for model selection UI in workflow step editor - Update AGENTS.md and README.md with model override documentation - Add changeset for published package bump
This commit is contained in:
@@ -9,7 +9,9 @@ import {
|
||||
fetchWorkflowStepTemplates,
|
||||
createWorkflowStepFromTemplate,
|
||||
fetchScripts,
|
||||
fetchModels,
|
||||
type WorkflowStepTemplate,
|
||||
type ModelInfo,
|
||||
} from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import {
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
Terminal,
|
||||
MessageSquare,
|
||||
} from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
|
||||
interface WorkflowStepManagerProps {
|
||||
isOpen: boolean;
|
||||
@@ -45,6 +48,8 @@ interface StepFormData {
|
||||
prompt: string;
|
||||
scriptName: string;
|
||||
enabled: boolean;
|
||||
modelProvider: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
type TabId = "my-steps" | "templates";
|
||||
@@ -56,8 +61,23 @@ const EMPTY_FORM: StepFormData = {
|
||||
prompt: "",
|
||||
scriptName: "",
|
||||
enabled: true,
|
||||
modelProvider: "",
|
||||
modelId: "",
|
||||
};
|
||||
|
||||
/** Build the combined "provider/modelId" value for CustomModelDropdown */
|
||||
function getModelDropdownValue(provider: string, modelId: string): string {
|
||||
return provider && modelId ? `${provider}/${modelId}` : "";
|
||||
}
|
||||
|
||||
/** Parse "provider/modelId" dropdown value back into separate fields */
|
||||
function parseModelDropdownValue(value: string): { provider: string; modelId: string } {
|
||||
if (!value) return { provider: "", modelId: "" };
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex === -1) return { provider: "", modelId: "" };
|
||||
return { provider: value.slice(0, slashIndex), modelId: value.slice(slashIndex + 1) };
|
||||
}
|
||||
|
||||
/** Map template icon names to Lucide components */
|
||||
function getTemplateIcon(iconName: string | undefined) {
|
||||
switch (iconName) {
|
||||
@@ -102,6 +122,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||
const [addingTemplateId, setAddingTemplateId] = useState<string | null>(null);
|
||||
const [availableScripts, setAvailableScripts] = useState<Record<string, string>>({});
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
|
||||
const loadSteps = useCallback(async () => {
|
||||
try {
|
||||
@@ -124,6 +145,15 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadModels = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetchModels();
|
||||
setAvailableModels(response.models || []);
|
||||
} catch {
|
||||
// Silently ignore — models are optional, dropdown will be empty
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadTemplates = useCallback(async () => {
|
||||
try {
|
||||
setTemplatesLoading(true);
|
||||
@@ -141,8 +171,9 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
loadSteps();
|
||||
loadTemplates();
|
||||
loadScripts();
|
||||
loadModels();
|
||||
}
|
||||
}, [isOpen, loadSteps, loadTemplates, loadScripts]);
|
||||
}, [isOpen, loadSteps, loadTemplates, loadScripts, loadModels]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
setIsCreating(true);
|
||||
@@ -160,6 +191,8 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
prompt: step.prompt,
|
||||
scriptName: step.scriptName || "",
|
||||
enabled: step.enabled,
|
||||
modelProvider: step.modelProvider || "",
|
||||
modelId: step.modelId || "",
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -177,6 +210,13 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// Build model override: only include when both fields are set (prompt mode)
|
||||
const modelFields = form.mode === "prompt" && form.modelProvider && form.modelId
|
||||
? { modelProvider: form.modelProvider, modelId: form.modelId }
|
||||
: form.mode === "prompt"
|
||||
? { modelProvider: undefined, modelId: undefined }
|
||||
: {};
|
||||
|
||||
if (isCreating) {
|
||||
const input: WorkflowStepInput = {
|
||||
name: form.name.trim(),
|
||||
@@ -185,6 +225,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
prompt: form.mode === "prompt" ? (form.prompt.trim() || undefined) : undefined,
|
||||
scriptName: form.mode === "script" ? form.scriptName.trim() : undefined,
|
||||
enabled: form.enabled,
|
||||
...modelFields,
|
||||
};
|
||||
await createWorkflowStep(input, projectId);
|
||||
addToast("Workflow step created", "success");
|
||||
@@ -196,6 +237,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
prompt: form.mode === "prompt" ? form.prompt : "",
|
||||
scriptName: form.mode === "script" ? form.scriptName.trim() : undefined,
|
||||
enabled: form.enabled,
|
||||
...modelFields,
|
||||
}, projectId);
|
||||
addToast("Workflow step updated", "success");
|
||||
}
|
||||
@@ -240,12 +282,17 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// Build model override for the intermediate create-then-refine flow
|
||||
const modelFields = form.modelProvider && form.modelId
|
||||
? { modelProvider: form.modelProvider, modelId: form.modelId }
|
||||
: {};
|
||||
const input: WorkflowStepInput = {
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim(),
|
||||
mode: "prompt",
|
||||
prompt: form.prompt.trim() || undefined,
|
||||
enabled: form.enabled,
|
||||
...modelFields,
|
||||
};
|
||||
const created = await createWorkflowStep(input, projectId);
|
||||
setIsCreating(false);
|
||||
@@ -548,7 +595,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${form.mode === "script" ? "btn-primary" : "btn-secondary"} wfm-mode-btn`}
|
||||
onClick={() => setForm((prev) => ({ ...prev, mode: "script", prompt: "" }))}
|
||||
onClick={() => setForm((prev) => ({ ...prev, mode: "script", prompt: "", modelProvider: "", modelId: "" }))}
|
||||
data-testid="mode-script"
|
||||
>
|
||||
<Terminal size={14} />
|
||||
@@ -589,6 +636,44 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model override (prompt mode only) */}
|
||||
{form.mode === "prompt" && (
|
||||
<div className="wfm-field" data-testid="workflow-step-model-field">
|
||||
<div className="wfm-model-header">
|
||||
<label>Model Override</label>
|
||||
{form.modelProvider && form.modelId && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon wfm-model-clear-btn"
|
||||
onClick={() => setForm((prev) => ({ ...prev, modelProvider: "", modelId: "" }))}
|
||||
title="Clear model override (use global default)"
|
||||
data-testid="clear-model-override"
|
||||
>
|
||||
<X size={12} />
|
||||
<span>Use default</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className="wfm-model-hint">
|
||||
{form.modelProvider && form.modelId
|
||||
? `Using ${form.modelProvider}/${form.modelId}`
|
||||
: "Using global default model"}
|
||||
</span>
|
||||
<div data-testid="workflow-step-model-select">
|
||||
<CustomModelDropdown
|
||||
models={availableModels}
|
||||
value={getModelDropdownValue(form.modelProvider, form.modelId)}
|
||||
onChange={(value: string) => {
|
||||
const parsed = parseModelDropdownValue(value);
|
||||
setForm((prev) => ({ ...prev, modelProvider: parsed.provider, modelId: parsed.modelId }));
|
||||
}}
|
||||
placeholder="Select a model override…"
|
||||
label="Model override for this workflow step"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Script selector (script mode only) */}
|
||||
{form.mode === "script" && (
|
||||
<div className="wfm-field">
|
||||
|
||||
@@ -49,6 +49,53 @@ vi.mock("../../api", () => ({
|
||||
workflowStep: { ...mockSteps[0], prompt: "AI-generated detailed prompt" },
|
||||
})),
|
||||
fetchScripts: vi.fn(() => Promise.resolve({ test: "pnpm test", lint: "pnpm lint" })),
|
||||
fetchModels: vi.fn(() => Promise.resolve({
|
||||
models: [
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: false, contextWindow: 200000 },
|
||||
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 },
|
||||
],
|
||||
favoriteProviders: [],
|
||||
favoriteModels: [],
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock CustomModelDropdown - renders a simple test-friendly control
|
||||
vi.mock("../CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
disabled,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
models?: unknown[];
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
favoriteProviders?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
favoriteModels?: string[];
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}) => (
|
||||
<div data-testid={`custom-model-dropdown-${label}`}>
|
||||
<span data-testid={`dropdown-value-${label}`}>{value || "none"}</span>
|
||||
<button
|
||||
data-testid={`dropdown-select-${label}`}
|
||||
onClick={() => onChange("anthropic/claude-sonnet-4-5")}
|
||||
disabled={disabled}
|
||||
>
|
||||
Select {label}
|
||||
</button>
|
||||
<button
|
||||
data-testid={`dropdown-clear-${label}`}
|
||||
onClick={() => onChange("")}
|
||||
>
|
||||
Clear {label}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
import {
|
||||
@@ -57,6 +104,7 @@ import {
|
||||
updateWorkflowStep,
|
||||
deleteWorkflowStep,
|
||||
refineWorkflowStepPrompt,
|
||||
fetchModels,
|
||||
} from "../../api";
|
||||
import { fetchScripts } from "../../api";
|
||||
|
||||
@@ -555,3 +603,295 @@ describe("WorkflowStepManager theme class structure", () => {
|
||||
expect(container.querySelector(".wfm-no-scripts")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("WorkflowStepManager model override", () => {
|
||||
it("shows model override field in prompt mode", async () => {
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("add-workflow-step"));
|
||||
|
||||
// Model override field should be visible in prompt mode
|
||||
expect(screen.getByTestId("workflow-step-model-field")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("workflow-step-model-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show model override field in script mode", async () => {
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("add-workflow-step"));
|
||||
fireEvent.click(screen.getByTestId("mode-script"));
|
||||
|
||||
// Model override field should NOT be visible in script mode
|
||||
expect(screen.queryByTestId("workflow-step-model-field")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears model override when switching from prompt to script mode", async () => {
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("add-workflow-step"));
|
||||
|
||||
// Select a model via the mock dropdown
|
||||
const selectBtn = screen.getByTestId("dropdown-select-Model override for this workflow step");
|
||||
fireEvent.click(selectBtn);
|
||||
|
||||
// Verify hint shows the selected model
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Using anthropic/claude-sonnet-4-5")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Switch to script mode
|
||||
fireEvent.click(screen.getByTestId("mode-script"));
|
||||
|
||||
// Switch back to prompt mode — model should be cleared
|
||||
fireEvent.click(screen.getByTestId("mode-prompt"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Using global default model")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("sends model override when creating a prompt-mode step with model selected", async () => {
|
||||
vi.mocked(fetchWorkflowSteps)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("add-workflow-step"));
|
||||
|
||||
const nameInput = screen.getByTestId("workflow-step-name");
|
||||
const descInput = screen.getByTestId("workflow-step-description");
|
||||
fireEvent.change(nameInput, { target: { value: "Security Scan" } });
|
||||
fireEvent.change(descInput, { target: { value: "Run security audit" } });
|
||||
|
||||
// Select a model via the mock dropdown
|
||||
const selectBtn = screen.getByTestId("dropdown-select-Model override for this workflow step");
|
||||
fireEvent.click(selectBtn);
|
||||
|
||||
fireEvent.click(screen.getByTestId("save-workflow-step"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWorkflowStep).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "Security Scan",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("sends undefined model fields when creating without model selection", async () => {
|
||||
vi.mocked(fetchWorkflowSteps)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("add-workflow-step"));
|
||||
|
||||
const nameInput = screen.getByTestId("workflow-step-name");
|
||||
const descInput = screen.getByTestId("workflow-step-description");
|
||||
fireEvent.change(nameInput, { target: { value: "QA Check" } });
|
||||
fireEvent.change(descInput, { target: { value: "Run tests" } });
|
||||
|
||||
fireEvent.click(screen.getByTestId("save-workflow-step"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWorkflowStep).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "QA Check",
|
||||
modelProvider: undefined,
|
||||
modelId: undefined,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("populates model fields when editing a step with model override", async () => {
|
||||
const stepWithModel: WorkflowStep[] = [
|
||||
{
|
||||
...mockSteps[0],
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
},
|
||||
];
|
||||
vi.mocked(fetchWorkflowSteps)
|
||||
.mockResolvedValueOnce(stepWithModel)
|
||||
.mockResolvedValueOnce(stepWithModel);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Documentation Review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Edit Documentation Review"));
|
||||
|
||||
// Model dropdown should show the existing override
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Using openai/gpt-4o")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows use-default hint when editing a step without model override", async () => {
|
||||
vi.mocked(fetchWorkflowSteps).mockReset().mockResolvedValueOnce(mockSteps);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Documentation Review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Edit Documentation Review"));
|
||||
|
||||
// Wait for the form to render with the model field
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-model-field")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Using global default model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends model override when updating a step", async () => {
|
||||
vi.mocked(fetchWorkflowSteps)
|
||||
.mockResolvedValueOnce(mockSteps)
|
||||
.mockResolvedValueOnce(mockSteps);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Documentation Review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Edit Documentation Review"));
|
||||
|
||||
// Select a model via the mock dropdown
|
||||
const selectBtn = screen.getByTestId("dropdown-select-Model override for this workflow step");
|
||||
fireEvent.click(selectBtn);
|
||||
|
||||
fireEvent.click(screen.getByTestId("save-workflow-step"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateWorkflowStep).toHaveBeenCalledWith(
|
||||
"WS-001",
|
||||
expect.objectContaining({
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("clears model override via clear button", async () => {
|
||||
const stepWithModel: WorkflowStep[] = [
|
||||
{
|
||||
...mockSteps[0],
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
},
|
||||
];
|
||||
vi.mocked(fetchWorkflowSteps)
|
||||
.mockReset()
|
||||
.mockResolvedValueOnce(stepWithModel)
|
||||
.mockResolvedValueOnce(stepWithModel);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Documentation Review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Edit Documentation Review"));
|
||||
|
||||
// Wait for the form to render with the model field
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-model-field")).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Using openai/gpt-4o")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click the clear button
|
||||
const clearBtn = screen.getByTestId("clear-model-override");
|
||||
fireEvent.click(clearBtn);
|
||||
|
||||
// Should now show default hint
|
||||
expect(screen.getByText("Using global default model")).toBeInTheDocument();
|
||||
|
||||
// Save should send undefined model fields
|
||||
fireEvent.click(screen.getByTestId("save-workflow-step"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateWorkflowStep).toHaveBeenCalledWith(
|
||||
"WS-001",
|
||||
expect.objectContaining({
|
||||
modelProvider: undefined,
|
||||
modelId: undefined,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows clear button only when model override is set", async () => {
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("add-workflow-step"));
|
||||
|
||||
// No model selected — clear button should NOT be visible
|
||||
expect(screen.queryByTestId("clear-model-override")).not.toBeInTheDocument();
|
||||
|
||||
// Select a model
|
||||
const selectBtn = screen.getByTestId("dropdown-select-Model override for this workflow step");
|
||||
fireEvent.click(selectBtn);
|
||||
|
||||
// Now clear button should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("clear-model-override")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("loads models via fetchModels on open", async () => {
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
|
||||
|
||||
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchModels).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user