Files
fusion/packages/dashboard/app/components/__tests__/AgentOnboardingModal.test.tsx
gsxdsm b98314923c FN-7899: add thinking-level editing to Agent Detail, Onboarding, and bulk task model selectors
Bring thinking-level (reasoning effort) editing to every remaining model selector surface that previously lacked it, so operators can set it consistently from Agent Detail, Agent Onboarding, and the List view's bulk task editor, in addition to the batch-update-models API and route that back them.

- Agent Detail config tab: persist/edit a built-in agent's runtimeConfig.thinkingLevel inline via the shared model dropdown, with dirty-state and reset tracking.
- Agent Onboarding modal: replace the read-only thinking-level input with an editable control wired into the same model dropdown used for creation.
- List view bulk edit toolbar: add a "no change" / "use default" / explicit-level thinking selector alongside executor/reviewer model and node overrides, wired through to the bulk apply action.
- Dashboard API client (`batchUpdateTaskModels`) and `/api/tasks/batch-update-models` route: accept and validate an optional `thinkingLevel` field (against `THINKING_LEVELS`), applying it per task alongside existing model/node updates.
- Update dashboard-guide.md docs and add regression tests across AgentDetailView, AgentOnboardingModal, ListView, and the batch-update-models route.
- Add a minor changeset documenting the feature for release notes.

Files changed:
 .changeset/thinking-level-selector-parity.md       |  7 ++
 docs/dashboard-guide.md                            |  5 +-
 packages/dashboard/app/api/legacy.ts               |  3 +
 .../dashboard/app/components/AgentDetailView.tsx   | 20 +++++-
 .../app/components/AgentOnboardingModal.tsx        | 11 +++-
 packages/dashboard/app/components/ListView.tsx     | 54 +++++++++++++---
 .../__tests__/AgentDetailView.settings.test.tsx    | 45 +++++++++++++
 .../__tests__/AgentDetailView.test-helpers.ts      | 19 +++++-
 .../__tests__/AgentOnboardingModal.test.tsx        | 41 +++++++++++-
 .../app/components/__tests__/ListView.test.tsx     | 43 ++++++++++++-
 .../src/__tests__/routes-tasks-ops.test.ts         | 75 ++++++++++++++++++++++
 .../src/routes/register-task-workflow-routes.ts    | 22 +++++--
 12 files changed, 323 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-7899

Fusion-Task-Lineage: fd584ce4-42b3-4c20-8de5-4d3c8963f593

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-12 19:18:25 -07:00

156 lines
6.0 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { AgentOnboardingModal } from "../AgentOnboardingModal";
import { createAgent } from "../../api";
let streamHandlers: any;
let respondCount = 0;
const originalScrollHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "scrollHeight");
vi.mock("../CustomModelDropdown", () => ({
CustomModelDropdown: ({ value, onChange, label, id, models, thinkingLevel, onThinkingLevelChange }: any) => (
<div data-testid="custom-model-dropdown" data-default-thinking-level="">
<label htmlFor={id}>{label}</label>
<select id={id} aria-label={label} value={value} onChange={(event) => onChange(event.target.value)}>
<option value="">Use default</option>
{models.map((model: any) => {
const modelValue = `${model.provider}/${model.id}`;
return <option key={modelValue} value={modelValue}>{modelValue}</option>;
})}
</select>
{onThinkingLevelChange ? (
<select aria-label={`${label} thinking level`} value={thinkingLevel ?? ""} onChange={(event) => onThinkingLevelChange(event.target.value)}>
{["off", "minimal", "low", "medium", "high", "xhigh"].map((level) => <option key={level} value={level}>{level}</option>)}
</select>
) : null}
</div>
),
}));
vi.mock("../../api", () => ({
startAgentOnboardingStreaming: vi.fn().mockResolvedValue({ sessionId: "onb-1" }),
fetchModels: vi.fn().mockResolvedValue({ models: [{ provider: "openai", id: "gpt-4o", name: "GPT-4o" }], favoriteProviders: [], favoriteModels: [] }),
connectAgentOnboardingStream: vi.fn().mockImplementation((_sessionId, _projectId, handlers) => {
streamHandlers = handlers;
setTimeout(() => handlers.onQuestion?.({ id: "q1", type: "text", question: "What should this agent primarily help with?" }), 0);
return { close: vi.fn(), isConnected: vi.fn(() => true) };
}),
respondToAgentOnboarding: vi.fn().mockImplementation(() => {
respondCount += 1;
if (respondCount === 1) {
setTimeout(() => streamHandlers?.onQuestion?.({ id: "q2", type: "text", question: "Second question" }), 0);
} else {
setTimeout(() => streamHandlers?.onSummary?.({
name: "Docs Reviewer",
role: "reviewer",
instructionsText: "Review docs",
thinkingLevel: "medium",
maxTurns: 20,
}), 0);
}
return Promise.resolve({ type: "question", data: {} });
}),
retryAgentOnboardingSession: vi.fn().mockResolvedValue({ success: true }),
stopAgentOnboardingGeneration: vi.fn().mockResolvedValue({ success: true }),
cancelAgentOnboarding: vi.fn().mockResolvedValue(undefined),
createAgent: vi.fn().mockResolvedValue({ id: "agent-1" }),
}));
afterEach(() => {
respondCount = 0;
streamHandlers = undefined;
vi.mocked(createAgent).mockClear();
if (originalScrollHeightDescriptor) {
Object.defineProperty(HTMLTextAreaElement.prototype, "scrollHeight", originalScrollHeightDescriptor);
} else {
Reflect.deleteProperty(HTMLTextAreaElement.prototype, "scrollHeight");
}
});
describe("AgentOnboardingModal", () => {
it("walks onboarding flow through summary and create", async () => {
const onCreated = vi.fn();
render(
<AgentOnboardingModal
isOpen={true}
onClose={vi.fn()}
onCreated={onCreated}
addToast={vi.fn()}
existingAgents={[]}
/>,
);
fireEvent.change(screen.getByLabelText("What do you want this agent to do?"), { target: { value: "Review docs" } });
fireEvent.click(screen.getByText("Start onboarding"));
await screen.findByText("What should this agent primarily help with?");
fireEvent.change(screen.getByLabelText("What should this agent primarily help with?"), { target: { value: "Docs" } });
fireEvent.click(screen.getByText("Continue"));
await screen.findByText("Second question");
fireEvent.change(screen.getByLabelText("Second question"), { target: { value: "More docs" } });
fireEvent.click(screen.getByText("Continue"));
await screen.findByText("Review generated configuration");
expect(screen.queryByLabelText("Thinking level")).not.toBeInTheDocument();
const thinkingSelect = screen.getByLabelText("Model thinking level");
expect(thinkingSelect).toHaveValue("medium");
fireEvent.change(thinkingSelect, { target: { value: "high" } });
fireEvent.change(screen.getByLabelText("Model"), { target: { value: "openai/gpt-4o" } });
fireEvent.click(screen.getByText("Create agent"));
await waitFor(() => {
expect(createAgent).toHaveBeenCalledWith(
expect.objectContaining({
runtimeConfig: expect.objectContaining({
thinkingLevel: "high",
model: "openai/gpt-4o",
}),
}),
undefined,
);
});
await waitFor(() => {
expect(onCreated).toHaveBeenCalled();
});
});
describe("autosize", () => {
it("grows the intent textarea up to the 640px cap", async () => {
Object.defineProperty(HTMLTextAreaElement.prototype, "scrollHeight", {
configurable: true,
get() {
const value = (this as HTMLTextAreaElement).value;
if (!value) return 24;
if (value.includes("cap")) return 800;
return 200;
},
});
render(
<AgentOnboardingModal
isOpen={true}
onClose={vi.fn()}
onCreated={vi.fn()}
addToast={vi.fn()}
existingAgents={[]}
/>,
);
const textarea = screen.getByLabelText("What do you want this agent to do?") as HTMLTextAreaElement;
await userEvent.type(textarea, "Draft onboarding intent");
await waitFor(() => {
expect(textarea.style.height).toBe("200px");
});
await userEvent.type(textarea, " cap");
await waitFor(() => {
expect(textarea.style.height).toBe("640px");
});
});
});
});