Fix workflow step execution wiring

This commit is contained in:
gsxdsm
2026-04-05 22:28:22 -07:00
parent 2f96f2da0d
commit d2b5d58699
9 changed files with 414 additions and 11 deletions

View File

@@ -1402,6 +1402,7 @@ describe("PATCH /tasks/:id", () => {
description: undefined,
prompt: undefined,
dependencies: ["FN-002"],
enabledWorkflowSteps: undefined,
modelProvider: null,
modelId: null,
validatorModelProvider: null,
@@ -1423,6 +1424,7 @@ describe("PATCH /tasks/:id", () => {
description: undefined,
prompt: undefined,
dependencies: undefined,
enabledWorkflowSteps: undefined,
modelProvider: null,
modelId: null,
validatorModelProvider: null,
@@ -1454,6 +1456,7 @@ describe("PATCH /tasks/:id", () => {
description: undefined,
prompt: undefined,
dependencies: undefined,
enabledWorkflowSteps: undefined,
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
@@ -1503,12 +1506,50 @@ describe("PATCH /tasks/:id", () => {
description: undefined,
prompt: undefined,
dependencies: undefined,
enabledWorkflowSteps: undefined,
modelProvider: null,
modelId: null,
validatorModelProvider: null,
validatorModelId: null,
});
});
it("forwards enabledWorkflowSteps to store.updateTask", async () => {
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
...FAKE_TASK_DETAIL,
enabledWorkflowSteps: ["browser-verification"],
});
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
enabledWorkflowSteps: ["browser-verification"],
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
title: undefined,
description: undefined,
prompt: undefined,
dependencies: undefined,
enabledWorkflowSteps: ["browser-verification"],
modelProvider: null,
modelId: null,
validatorModelProvider: null,
validatorModelId: null,
});
});
it("returns 400 for invalid enabledWorkflowSteps type", async () => {
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
enabledWorkflowSteps: [123],
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(400);
expect(res.body.error).toContain("enabledWorkflowSteps must be an array of strings");
});
});
describe("Attachment routes", () => {
@@ -6941,9 +6982,11 @@ describe("POST /workflow-step-templates/:id/create", () => {
expect(res.body.id).toBe("WS-001");
expect(res.body.name).toBe("Documentation Review");
expect(store.createWorkflowStep).toHaveBeenCalledWith({
templateId: "documentation-review",
name: "Documentation Review",
description: "Verify all public APIs, functions, and complex logic have appropriate documentation",
prompt: expect.stringContaining("documentation reviewer"),
toolMode: "readonly",
enabled: true,
});
});
@@ -6968,9 +7011,11 @@ describe("POST /workflow-step-templates/:id/create", () => {
expect(res.status).toBe(201);
expect(res.body.name).toBe("QA Check");
expect(store.createWorkflowStep).toHaveBeenCalledWith({
templateId: "qa-check",
name: "QA Check",
description: "Run tests and verify they pass, check for obvious bugs",
prompt: expect.stringContaining("QA tester"),
toolMode: "coding",
enabled: true,
});
});

View File

@@ -1744,7 +1744,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
);
res.status(201).json(task);
} catch (err: any) {
const status = err.message?.includes("must be a string") ? 400 : 500;
const status = err.message?.includes("must be a string") || err.message?.includes("must be an array of strings") ? 400 : 500;
res.status(status).json({ error: err.message });
}
});
@@ -2457,7 +2457,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
router.patch("/tasks/:id", async (req, res) => {
try {
const scopedStore = await getScopedStore(req);
const { title, description, prompt, dependencies, modelProvider, modelId, validatorModelProvider, validatorModelId } = req.body;
const { title, description, prompt, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId } = req.body;
// Validate model fields are strings or undefined/null
const validateModelField = (value: unknown, name: string): string | null | undefined => {
@@ -2473,11 +2473,18 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
const validatedValidatorModelProvider = validateModelField(validatorModelProvider, "validatorModelProvider");
const validatedValidatorModelId = validateModelField(validatorModelId, "validatorModelId");
if (enabledWorkflowSteps !== undefined) {
if (!Array.isArray(enabledWorkflowSteps) || !enabledWorkflowSteps.every((id: unknown) => typeof id === "string")) {
throw new Error("enabledWorkflowSteps must be an array of strings");
}
}
const task = await scopedStore.updateTask(req.params.id, {
title,
description,
prompt,
dependencies,
enabledWorkflowSteps,
modelProvider: validatedModelProvider,
modelId: validatedModelId,
validatorModelProvider: validatedValidatorModelProvider,
@@ -2485,7 +2492,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
});
res.json(task);
} catch (err: any) {
const status = err.message?.includes("must be a string") ? 400 : 500;
const status = err.message?.includes("must be a string") || err.message?.includes("must be an array of strings") ? 400 : 500;
res.status(status).json({ error: err.message });
}
});
@@ -6205,7 +6212,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, defaultOn, modelProvider, modelId } = req.body;
const { name, description, mode, phase, prompt, toolMode, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
if (!name || typeof name !== "string" || !name.trim()) {
res.status(400).json({ error: "name is required" });
@@ -6233,6 +6240,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
res.status(400).json({ error: "prompt must be a string" });
return;
}
if (toolMode !== undefined && toolMode !== "readonly" && toolMode !== "coding") {
res.status(400).json({ error: "toolMode must be 'readonly' or 'coding'" });
return;
}
if (scriptName !== undefined && typeof scriptName !== "string") {
res.status(400).json({ error: "scriptName must be a string" });
return;
@@ -6276,6 +6287,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
mode: resolvedMode,
phase,
prompt: prompt?.trim(),
toolMode,
scriptName: scriptName?.trim(),
enabled,
defaultOn: defaultOn === true,
@@ -6298,7 +6310,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, defaultOn, modelProvider, modelId } = req.body;
const { name, description, mode, phase, prompt, toolMode, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
const updates: Record<string, unknown> = {};
if (name !== undefined) {
@@ -6336,6 +6348,13 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
updates.prompt = prompt;
}
if (toolMode !== undefined) {
if (toolMode !== "readonly" && toolMode !== "coding") {
res.status(400).json({ error: "toolMode must be 'readonly' or 'coding'" });
return;
}
updates.toolMode = toolMode;
}
if (scriptName !== undefined) {
if (typeof scriptName !== "string") {
res.status(400).json({ error: "scriptName must be a string" });
@@ -6537,9 +6556,11 @@ Output ONLY the prompt text (no markdown, no explanations).`;
}
const step = await scopedStore.createWorkflowStep({
templateId: template.id,
name: template.name,
description: template.description,
prompt: template.prompt,
toolMode: template.toolMode,
enabled: true,
});