fix(FN-819): remove stale tests and add regression coverage for new-task surfaces
- Replace obsolete test files (SettingsModal, TaskCard, App, dashboard-footer-mobile-layout) with targeted tests for NewTaskModal and TaskForm - Fix SettingsModal component reference and update TaskCard preset badge logic - Clean up dead code in engine pi.ts and its test - Add preset indicator styles in dashboard CSS - Update dashboard README to clarify preset availability across all new-task surfaces
This commit is contained in:
@@ -43,7 +43,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
|
||||
- **Inline Editing**: Quick-edit a task's description directly on the board for Triage and Todo columns. The editor opens as a taller multi-line editing area (4 visible lines) for comfortable editing of longer descriptions, and auto-grows to fit existing content. Double-click a card or use the pencil icon — visible on hover for desktop, always visible on mobile for touch accessibility. Inline editing changes only the description; the title is preserved. To edit both title and description, use the task detail modal.
|
||||
- **Task Detail Editing**: Edit task title and description directly in the task detail modal. Click the pencil icon in the modal header (available for Triage and Todo tasks) to enter edit mode. Save and Cancel actions appear in the modal footer alongside a keyboard shortcut hint, keeping editing controls consistent with other modal action patterns.
|
||||
- **List View**: Alternative tabular view for tasks with sorting and filtering. The "Hide Done" toggle hides both Done and Archived tasks for an active-work-only view.
|
||||
- **Model Selection at Creation**: Choose executor and validator AI models while creating tasks from the board or list view, or leave them unset to use the global defaults. Quick-add model dropdowns in both the board triage column and the list view honor saved favorite providers and pinned models, matching the rest of the dashboard model UI. Saved model presets are available directly inside the model selection modal for quick-add surfaces (QuickEntryBox and InlineCreateCard), so users can apply a preset's executor/validator configuration in one click without navigating to the full task form.
|
||||
- **Model Selection at Creation**: Choose executor and validator AI models while creating tasks from the board or list view, or leave them unset to use the global defaults. Quick-add model dropdowns in both the board triage column and the list view honor saved favorite providers and pinned models, matching the rest of the dashboard model UI. Saved model presets are available in every new-task model-selection surface — the full New Task form (`TaskForm`/`NewTaskModal`), the board inline create card (`InlineCreateCard`), and the list-view quick entry box (`QuickEntryBox`). Users can choose between default behavior, a saved preset (which applies its executor/validator values in one click), or custom per-model overrides; returning to default clears all overrides, and manual model selection exits preset mode cleanly.
|
||||
- **AI-Assisted Creation Controls**: Plan, Subtask, and Refine buttons appear directly below the description textarea in all task creation surfaces (quick entry box, inline create card, and task form modal). These description-adjacent controls make AI-assisted creation and refinement feel directly associated with the text being edited. Deps, Models, and Save actions remain in the expanded controls footer.
|
||||
- **Layered Model Dropdowns**: Shared model combobox menus render in a top-level portal attached to `document.body`, so they stay above board columns and scrollable modal content instead of being clipped behind surrounding dashboard surfaces.
|
||||
- **Bulk Model Editing**: Update AI model configuration for multiple tasks at once in the list view. Select tasks via checkboxes (archived tasks excluded), then use the "Bulk Edit Models" toolbar to apply executor and/or validator model changes to all selected tasks. Selection persists in localStorage across page reloads.
|
||||
|
||||
@@ -313,4 +313,106 @@ describe("NewTaskModal", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Preset selection tests (FN-819)
|
||||
describe("model preset selection payload", () => {
|
||||
it("omits modelPresetId from payload when in default mode", async () => {
|
||||
const { props } = renderNewTaskModal();
|
||||
|
||||
const descTextarea = screen.getByLabelText(/Description/i);
|
||||
fireEvent.change(descTextarea, { target: { value: "Default mode task" } });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreateTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelPresetId: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("includes modelPresetId and model overrides in payload when preset is selected", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValue({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
const { props } = renderNewTaskModal();
|
||||
|
||||
// Wait for settings to load and preset dropdown to populate
|
||||
await waitFor(() => {
|
||||
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
expect(select).toBeTruthy();
|
||||
expect(Array.from(select.options).some((o) => o.value === "fast")).toBe(true);
|
||||
});
|
||||
|
||||
// Type a description
|
||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Preset task" } });
|
||||
|
||||
// Select the preset
|
||||
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
fireEvent.change(select, { target: { value: "fast" } });
|
||||
|
||||
// Submit
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreateTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelPresetId: "fast",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("omits modelPresetId from payload when switching from preset to custom", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValue({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
const { props } = renderNewTaskModal();
|
||||
|
||||
// Wait for settings to load
|
||||
await waitFor(() => {
|
||||
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
expect(Array.from(select.options).some((o) => o.value === "fast")).toBe(true);
|
||||
});
|
||||
|
||||
// Type a description
|
||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Custom task" } });
|
||||
|
||||
// Select a preset first
|
||||
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
fireEvent.change(select, { target: { value: "fast" } });
|
||||
|
||||
// Now switch to custom
|
||||
fireEvent.change(select, { target: { value: "custom" } });
|
||||
|
||||
// Submit
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreateTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelPresetId: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -321,3 +321,215 @@ describe("TaskForm description-adjacent actions layout (FN-781)", () => {
|
||||
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskForm preset selection (FN-819)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders preset dropdown with saved presets from settings", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
renderTaskForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const presetSelect = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
expect(presetSelect).toBeTruthy();
|
||||
const options = Array.from(presetSelect.options);
|
||||
expect(options.find((o) => o.value === "default")).toBeTruthy();
|
||||
expect(options.find((o) => o.value === "fast")).toBeTruthy();
|
||||
expect(options.find((o) => o.textContent === "Fast")).toBeTruthy();
|
||||
expect(options.find((o) => o.value === "custom")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("selecting a preset applies preset mode and model overrides", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
const onPresetModeChange = vi.fn();
|
||||
const onSelectedPresetIdChange = vi.fn();
|
||||
const onExecutorModelChange = vi.fn();
|
||||
const onValidatorModelChange = vi.fn();
|
||||
|
||||
renderTaskForm({
|
||||
onPresetModeChange,
|
||||
onSelectedPresetIdChange,
|
||||
onExecutorModelChange,
|
||||
onValidatorModelChange,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const presetSelect = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
fireEvent.change(presetSelect, { target: { value: "fast" } });
|
||||
|
||||
expect(onPresetModeChange).toHaveBeenCalledWith("preset");
|
||||
expect(onSelectedPresetIdChange).toHaveBeenCalledWith("fast");
|
||||
expect(onExecutorModelChange).toHaveBeenCalledWith("anthropic/claude-sonnet-4-5");
|
||||
expect(onValidatorModelChange).toHaveBeenCalledWith("openai/gpt-4o");
|
||||
});
|
||||
|
||||
it("switching to default clears preset and model overrides", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
const onPresetModeChange = vi.fn();
|
||||
const onSelectedPresetIdChange = vi.fn();
|
||||
const onExecutorModelChange = vi.fn();
|
||||
const onValidatorModelChange = vi.fn();
|
||||
|
||||
renderTaskForm({
|
||||
presetMode: "preset",
|
||||
selectedPresetId: "fast",
|
||||
executorModel: "anthropic/claude-sonnet-4-5",
|
||||
onPresetModeChange,
|
||||
onSelectedPresetIdChange,
|
||||
onExecutorModelChange,
|
||||
onValidatorModelChange,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const presetSelect = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
fireEvent.change(presetSelect, { target: { value: "default" } });
|
||||
|
||||
expect(onPresetModeChange).toHaveBeenCalledWith("default");
|
||||
expect(onSelectedPresetIdChange).toHaveBeenCalledWith("");
|
||||
expect(onExecutorModelChange).toHaveBeenCalledWith("");
|
||||
expect(onValidatorModelChange).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("switching to custom clears preset ID", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
const onPresetModeChange = vi.fn();
|
||||
const onSelectedPresetIdChange = vi.fn();
|
||||
|
||||
renderTaskForm({
|
||||
presetMode: "preset",
|
||||
selectedPresetId: "fast",
|
||||
executorModel: "anthropic/claude-sonnet-4-5",
|
||||
onPresetModeChange,
|
||||
onSelectedPresetIdChange,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const presetSelect = document.getElementById("model-preset") as HTMLSelectElement;
|
||||
fireEvent.change(presetSelect, { target: { value: "custom" } });
|
||||
|
||||
expect(onPresetModeChange).toHaveBeenCalledWith("custom");
|
||||
expect(onSelectedPresetIdChange).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("Override button exits preset mode", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
const onPresetModeChange = vi.fn();
|
||||
|
||||
renderTaskForm({
|
||||
presetMode: "preset",
|
||||
selectedPresetId: "fast",
|
||||
executorModel: "anthropic/claude-sonnet-4-5",
|
||||
onPresetModeChange,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const overrideButton = screen.getByRole("button", { name: "Override" });
|
||||
fireEvent.click(overrideButton);
|
||||
|
||||
expect(onPresetModeChange).toHaveBeenCalledWith("custom");
|
||||
});
|
||||
|
||||
it("disables executor and validator selects when preset mode is active", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
renderTaskForm({
|
||||
presetMode: "preset",
|
||||
selectedPresetId: "fast",
|
||||
executorModel: "anthropic/claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const executorSelect = document.getElementById("executor-model") as HTMLSelectElement;
|
||||
const validatorSelect = document.getElementById("validator-model") as HTMLSelectElement;
|
||||
expect(executorSelect?.disabled).toBe(true);
|
||||
expect(validatorSelect?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("shows preset name as small text when a preset is selected", async () => {
|
||||
const { fetchSettings } = await import("../../api");
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
modelPresets: [
|
||||
{ id: "fast", name: "Fast", executorProvider: "anthropic", executorModelId: "claude-sonnet-4-5" },
|
||||
],
|
||||
autoSelectModelPreset: false,
|
||||
defaultPresetBySize: {},
|
||||
});
|
||||
|
||||
renderTaskForm({
|
||||
presetMode: "preset",
|
||||
selectedPresetId: "fast",
|
||||
executorModel: "anthropic/claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Using preset: Fast")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user