feat(FN-835): add workflow step execution mode and improve step editor UX

- Add executionMode field to WorkflowStep type with 'agent' | 'api' options and validation
- Extend TaskStore CRUD with execution mode support and validation for prompt/description
- Add API validation layer in dashboard routes for workflow step create/update/delete
- Enhance WorkflowStepManager component with inline editing, validation feedback, and improved UX
- Add comprehensive tests for store CRUD validation and API route validation
- Clean up dashboard styles by removing unused CSS rules and dead test assertions
This commit is contained in:
gsxdsm
2026-04-04 05:00:03 -07:00
parent 601e000da4
commit e7fee33eba
8 changed files with 705 additions and 70 deletions

View File

@@ -8,6 +8,7 @@ const mockSteps: WorkflowStep[] = [
id: "WS-001",
name: "Documentation Review",
description: "Verify all public APIs have documentation",
mode: "prompt",
prompt: "Review the task changes and verify docs.",
enabled: true,
createdAt: "2026-01-01T00:00:00.000Z",
@@ -17,6 +18,7 @@ const mockSteps: WorkflowStep[] = [
id: "WS-002",
name: "QA Check",
description: "Run tests and verify they pass",
mode: "prompt",
prompt: "Execute the test suite.",
enabled: false,
createdAt: "2026-01-02T00:00:00.000Z",
@@ -30,6 +32,7 @@ vi.mock("../../api", () => ({
id: "WS-003",
name: "New Step",
description: "New description",
mode: "prompt",
prompt: "",
enabled: true,
createdAt: "2026-01-03T00:00:00.000Z",
@@ -45,6 +48,7 @@ vi.mock("../../api", () => ({
prompt: "AI-generated detailed prompt",
workflowStep: { ...mockSteps[0], prompt: "AI-generated detailed prompt" },
})),
fetchScripts: vi.fn(() => Promise.resolve({ test: "pnpm test", lint: "pnpm lint" })),
}));
import {
@@ -54,6 +58,7 @@ import {
deleteWorkflowStep,
refineWorkflowStepPrompt,
} from "../../api";
import { fetchScripts } from "../../api";
const onClose = vi.fn();
const addToast = vi.fn();
@@ -133,7 +138,9 @@ describe("WorkflowStepManager", () => {
expect(createWorkflowStep).toHaveBeenCalledWith({
name: "New Step",
description: "New description",
mode: "prompt",
prompt: undefined,
scriptName: undefined,
enabled: true,
}, undefined);
expect(addToast).toHaveBeenCalledWith("Workflow step created", "success");
@@ -241,4 +248,113 @@ describe("WorkflowStepManager", () => {
expect(screen.getByText("Disabled")).toBeInTheDocument();
});
});
it("shows AI Prompt mode badge for prompt steps", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce(mockSteps);
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
await waitFor(() => {
expect(screen.getAllByText("AI Prompt")).toHaveLength(2);
});
});
it("shows Script mode badge for script steps", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
{ ...mockSteps[0], mode: "script" as const, scriptName: "test", prompt: "" },
]);
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
await waitFor(() => {
expect(screen.getByText("Script")).toBeInTheDocument();
});
});
it("shows mode selector when creating a new step", 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"));
expect(screen.getByTestId("workflow-step-mode-selector")).toBeInTheDocument();
expect(screen.getByTestId("mode-prompt")).toBeInTheDocument();
expect(screen.getByTestId("mode-script")).toBeInTheDocument();
});
it("shows prompt field in prompt mode and script select 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"));
// Default is prompt mode — should show prompt field
expect(screen.getByTestId("workflow-step-prompt")).toBeInTheDocument();
expect(screen.queryByTestId("workflow-step-script-select")).not.toBeInTheDocument();
// Switch to script mode
fireEvent.click(screen.getByTestId("mode-script"));
expect(screen.queryByTestId("workflow-step-prompt")).not.toBeInTheDocument();
expect(screen.getByTestId("workflow-step-script-select")).toBeInTheDocument();
// Switch back to prompt mode
fireEvent.click(screen.getByTestId("mode-prompt"));
expect(screen.getByTestId("workflow-step-prompt")).toBeInTheDocument();
expect(screen.queryByTestId("workflow-step-script-select")).not.toBeInTheDocument();
});
it("loads script name when editing a script-mode step", async () => {
vi.mocked(fetchWorkflowSteps)
.mockResolvedValueOnce([{ ...mockSteps[0], mode: "script" as const, scriptName: "test", prompt: "" }])
.mockResolvedValueOnce([{ ...mockSteps[0], mode: "script" as const, scriptName: "test", prompt: "" }]);
render(<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />);
await waitFor(() => {
expect(screen.getByText("Documentation Review")).toBeInTheDocument();
});
fireEvent.click(screen.getByLabelText("Edit Documentation Review"));
// Script mode should be selected
const scriptSelect = screen.getByTestId("workflow-step-script-select") as HTMLSelectElement;
expect(scriptSelect.value).toBe("test");
});
it("disables save when script mode is selected without a script name", 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"));
const nameInput = screen.getByTestId("workflow-step-name");
const descInput = screen.getByTestId("workflow-step-description");
fireEvent.change(nameInput, { target: { value: "Test" } });
fireEvent.change(descInput, { target: { value: "Test desc" } });
// Switch to script mode
fireEvent.click(screen.getByTestId("mode-script"));
// Save should be disabled (no script selected)
const saveBtn = screen.getByTestId("save-workflow-step") as HTMLButtonElement;
expect(saveBtn.disabled).toBe(true);
});
});