feat(FN-835): add script-mode workflow step execution with validation hardening
- Implement script-mode workflow steps that execute named commands from project settings with 2-minute timeout - Harden PATCH /api/workflow-steps/:id validation to reject empty names, check resulting state validity - Add comprehensive executor tests (420 lines) covering script mode execution, timeout, missing scripts, and prompt mode - Add dashboard route tests for PATCH validation edge cases - Update AGENTS.md and README.md with script-mode engine behavior documentation
This commit is contained in:
@@ -6503,6 +6503,55 @@ describe("PATCH /workflow-steps/:id", () => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("must include both provider and modelId");
|
||||
});
|
||||
|
||||
it("returns 400 when updating scriptName to nonexistent on existing script-mode step", async () => {
|
||||
// Simulate an existing script-mode step
|
||||
(store.getWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
id: "WS-001",
|
||||
name: "Run Tests",
|
||||
description: "Test runner",
|
||||
mode: "script",
|
||||
scriptName: "test",
|
||||
prompt: "",
|
||||
enabled: true,
|
||||
createdAt: "2026-01-01",
|
||||
updatedAt: "2026-01-01",
|
||||
});
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
scripts: { test: "pnpm test", lint: "pnpm lint" },
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
scriptName: "nonexistent",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("not found in project settings");
|
||||
// Should NOT have called updateWorkflowStep since validation failed
|
||||
expect(store.updateWorkflowStep).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 400 when updating script-mode step without scriptName (resulting state)", async () => {
|
||||
// Simulate an existing script-mode step with scriptName cleared
|
||||
(store.getWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
id: "WS-001",
|
||||
name: "Run Tests",
|
||||
description: "Test runner",
|
||||
mode: "script",
|
||||
scriptName: "",
|
||||
prompt: "",
|
||||
enabled: true,
|
||||
createdAt: "2026-01-01",
|
||||
updatedAt: "2026-01-01",
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
name: "Updated Name",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("scriptName is required when mode is 'script'");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /workflow-steps/:id", () => {
|
||||
|
||||
@@ -5968,17 +5968,22 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
updates.enabled = enabled;
|
||||
}
|
||||
|
||||
// Validate script name references an actual script when switching to script mode
|
||||
if (updates.mode === "script") {
|
||||
const scriptNameToValidate = (updates.scriptName as string | undefined);
|
||||
if (!scriptNameToValidate?.trim()) {
|
||||
// Validate script-mode requirements against the resulting state (existing + updates)
|
||||
// This catches cases where an existing script-mode step has its scriptName updated
|
||||
// without the mode field being explicitly sent.
|
||||
const existingStep = await scopedStore.getWorkflowStep(req.params.id);
|
||||
const resultingMode: string | undefined = updates.mode !== undefined ? (updates.mode as string) : existingStep?.mode;
|
||||
const resultingScriptName: string | undefined = updates.scriptName !== undefined ? (updates.scriptName as string) : existingStep?.scriptName;
|
||||
|
||||
if (resultingMode === "script") {
|
||||
if (!resultingScriptName?.trim()) {
|
||||
res.status(400).json({ error: "scriptName is required when mode is 'script'" });
|
||||
return;
|
||||
}
|
||||
const settings = await scopedStore.getSettings();
|
||||
const scripts = settings.scripts || {};
|
||||
if (!(scriptNameToValidate.trim() in scripts)) {
|
||||
res.status(400).json({ error: `Script '${scriptNameToValidate.trim()}' not found in project settings. Available scripts: ${Object.keys(scripts).join(", ") || "none"}` });
|
||||
if (!(resultingScriptName.trim() in scripts)) {
|
||||
res.status(400).json({ error: `Script '${resultingScriptName.trim()}' not found in project settings. Available scripts: ${Object.keys(scripts).join(", ") || "none"}` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user