feat(FN-883): add workflow step default-on support with task creation opt-out
- Add defaultOn field to WorkflowStep model, API endpoints (GET/POST/PATCH), and WorkflowStepManager UI - Add skip-default-steps checkbox to TaskForm and NewTaskModal for explicit opt-out of default workflow steps - Pre-select default workflow steps on new task creation forms automatically - Add CSS styling for skip-default-steps toggle row - Add comprehensive tests for WorkflowStepManager, TaskForm, and NewTaskModal components - Add route tests for PATCH workflow-step endpoint and default selection logic - Update README and task-structure reference docs
This commit is contained in:
@@ -6313,6 +6313,7 @@ describe("POST /workflow-steps", () => {
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
defaultOn: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6368,6 +6369,7 @@ describe("POST /workflow-steps", () => {
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
defaultOn: false,
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
@@ -6415,6 +6417,7 @@ describe("POST /workflow-steps", () => {
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
defaultOn: false,
|
||||
modelProvider: undefined,
|
||||
modelId: undefined,
|
||||
});
|
||||
@@ -6443,6 +6446,7 @@ describe("POST /workflow-steps", () => {
|
||||
prompt: undefined,
|
||||
scriptName: "test",
|
||||
enabled: undefined,
|
||||
defaultOn: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6503,6 +6507,7 @@ describe("POST /workflow-steps", () => {
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
defaultOn: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6516,6 +6521,55 @@ describe("POST /workflow-steps", () => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("phase must be");
|
||||
});
|
||||
|
||||
it("creates a workflow step with defaultOn true", async () => {
|
||||
const created = { id: "WS-010", name: "Auto Step", description: "Auto-enabled", mode: "prompt", prompt: "", enabled: true, defaultOn: true, createdAt: "2026-01-01", updatedAt: "2026-01-01" };
|
||||
(store.createWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce(created);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/workflow-steps", JSON.stringify({
|
||||
name: "Auto Step",
|
||||
description: "Auto-enabled",
|
||||
defaultOn: true,
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(store.createWorkflowStep).toHaveBeenCalledWith({
|
||||
name: "Auto Step",
|
||||
description: "Auto-enabled",
|
||||
mode: "prompt",
|
||||
phase: undefined,
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
defaultOn: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults defaultOn to false when not specified", async () => {
|
||||
const created = { id: "WS-011", name: "Manual Step", description: "Manual only", mode: "prompt", prompt: "", enabled: true, createdAt: "2026-01-01", updatedAt: "2026-01-01" };
|
||||
(store.createWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce(created);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/workflow-steps", JSON.stringify({
|
||||
name: "Manual Step",
|
||||
description: "Manual only",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(store.createWorkflowStep).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ defaultOn: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 400 when defaultOn is not a boolean", async () => {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/workflow-steps", JSON.stringify({
|
||||
name: "Bad Step",
|
||||
description: "Bad defaultOn",
|
||||
defaultOn: "yes",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("defaultOn");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /workflow-steps/:id", () => {
|
||||
@@ -6668,6 +6722,39 @@ describe("PATCH /workflow-steps/:id", () => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("phase must be");
|
||||
});
|
||||
|
||||
it("updates defaultOn to true", async () => {
|
||||
const updated = { id: "WS-001", name: "Docs", description: "Check docs", mode: "prompt", prompt: "", enabled: true, defaultOn: true, createdAt: "2026-01-01", updatedAt: "2026-01-01" };
|
||||
(store.updateWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated);
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
defaultOn: true,
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateWorkflowStep).toHaveBeenCalledWith("WS-001", expect.objectContaining({ defaultOn: true }));
|
||||
});
|
||||
|
||||
it("updates defaultOn to false", async () => {
|
||||
const updated = { id: "WS-001", name: "Docs", description: "Check docs", mode: "prompt", prompt: "", enabled: true, defaultOn: false, createdAt: "2026-01-01", updatedAt: "2026-01-01" };
|
||||
(store.updateWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated);
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
defaultOn: false,
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateWorkflowStep).toHaveBeenCalledWith("WS-001", expect.objectContaining({ defaultOn: false }));
|
||||
});
|
||||
|
||||
it("returns 400 when defaultOn is not a boolean in PATCH", async () => {
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
defaultOn: "yes",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("defaultOn");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /workflow-steps/:id", () => {
|
||||
|
||||
@@ -6001,7 +6001,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
router.post("/workflow-steps", async (req, res) => {
|
||||
try {
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { name, description, mode, phase, prompt, scriptName, enabled, modelProvider, modelId } = req.body;
|
||||
const { name, description, mode, phase, prompt, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
||||
|
||||
if (!name || typeof name !== "string" || !name.trim()) {
|
||||
res.status(400).json({ error: "name is required" });
|
||||
@@ -6037,6 +6037,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
res.status(400).json({ error: "enabled must be a boolean" });
|
||||
return;
|
||||
}
|
||||
if (defaultOn !== undefined && typeof defaultOn !== "boolean") {
|
||||
res.status(400).json({ error: "defaultOn must be a boolean" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate script mode: scriptName must reference a named script in settings
|
||||
if (resolvedMode === "script") {
|
||||
@@ -6070,6 +6074,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
prompt: prompt?.trim(),
|
||||
scriptName: scriptName?.trim(),
|
||||
enabled,
|
||||
defaultOn: defaultOn === true,
|
||||
modelProvider: modelPair.provider,
|
||||
modelId: modelPair.modelId,
|
||||
});
|
||||
@@ -6089,7 +6094,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
router.patch("/workflow-steps/:id", async (req, res) => {
|
||||
try {
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { name, description, mode, phase, prompt, scriptName, enabled, modelProvider, modelId } = req.body;
|
||||
const { name, description, mode, phase, prompt, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (name !== undefined) {
|
||||
@@ -6141,6 +6146,13 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
updates.enabled = enabled;
|
||||
}
|
||||
if (defaultOn !== undefined) {
|
||||
if (typeof defaultOn !== "boolean") {
|
||||
res.status(400).json({ error: "defaultOn must be a boolean" });
|
||||
return;
|
||||
}
|
||||
updates.defaultOn = defaultOn;
|
||||
}
|
||||
|
||||
// Validate script-mode requirements against the resulting state (existing + updates)
|
||||
// This catches cases where an existing script-mode step has its scriptName updated
|
||||
|
||||
Reference in New Issue
Block a user