feat(FN-837): add workflow step phase support with pre-merge and post-merge execution
- Add phase field to workflow step definitions (pre-merge vs post-merge) with persistence and API - Execute pre-merge steps in executor before merge; post-merge steps in merger after successful merge - Pre-merge failures block merge and keep task in in-review; post-merge failures are logged only - Expose phase controls and phase-aware results in the dashboard UI - Add changeset for the published @gsxdsm/fusion package
This commit is contained in:
@@ -6262,6 +6262,7 @@ describe("POST /workflow-steps", () => {
|
||||
name: "Docs",
|
||||
description: "Check docs",
|
||||
mode: "prompt",
|
||||
phase: undefined,
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
@@ -6316,6 +6317,7 @@ describe("POST /workflow-steps", () => {
|
||||
name: "Security",
|
||||
description: "Security audit",
|
||||
mode: "prompt",
|
||||
phase: undefined,
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
@@ -6362,6 +6364,7 @@ describe("POST /workflow-steps", () => {
|
||||
name: "Docs",
|
||||
description: "Check docs",
|
||||
mode: "prompt",
|
||||
phase: undefined,
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
@@ -6389,6 +6392,7 @@ describe("POST /workflow-steps", () => {
|
||||
name: "Run Tests",
|
||||
description: "Execute tests",
|
||||
mode: "script",
|
||||
phase: undefined,
|
||||
prompt: undefined,
|
||||
scriptName: "test",
|
||||
enabled: undefined,
|
||||
@@ -6432,6 +6436,39 @@ describe("POST /workflow-steps", () => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("mode must be");
|
||||
});
|
||||
|
||||
it("creates a workflow step with 'post-merge' phase", async () => {
|
||||
const created = { id: "WS-001", name: "Post Merge", description: "After merge", mode: "prompt", phase: "post-merge", 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: "Post Merge",
|
||||
description: "After merge",
|
||||
phase: "post-merge",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(store.createWorkflowStep).toHaveBeenCalledWith({
|
||||
name: "Post Merge",
|
||||
description: "After merge",
|
||||
mode: "prompt",
|
||||
phase: "post-merge",
|
||||
prompt: undefined,
|
||||
scriptName: undefined,
|
||||
enabled: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 400 for invalid phase value", async () => {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/workflow-steps", JSON.stringify({
|
||||
name: "Test",
|
||||
description: "Test",
|
||||
phase: "during-merge",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("phase must be");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /workflow-steps/:id", () => {
|
||||
@@ -6554,6 +6591,36 @@ describe("PATCH /workflow-steps/:id", () => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("scriptName is required when mode is 'script'");
|
||||
});
|
||||
|
||||
it("updates a workflow step phase", async () => {
|
||||
const updated = { id: "WS-001", name: "Post Merge", description: "After merge", phase: "post-merge", createdAt: "2026-01-01", updatedAt: "2026-01-02" };
|
||||
(store.updateWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated);
|
||||
(store.getWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
id: "WS-001", name: "Pre Merge", description: "Before merge", mode: "prompt", prompt: "", enabled: true, createdAt: "2026-01-01", updatedAt: "2026-01-01",
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
phase: "post-merge",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateWorkflowStep).toHaveBeenCalledWith("WS-001", expect.objectContaining({
|
||||
phase: "post-merge",
|
||||
}));
|
||||
});
|
||||
|
||||
it("returns 400 for invalid phase value on update", async () => {
|
||||
(store.getWorkflowStep as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
id: "WS-001", name: "Test", description: "Test", mode: "prompt", prompt: "", enabled: true, createdAt: "2026-01-01", updatedAt: "2026-01-01",
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/workflow-steps/WS-001", JSON.stringify({
|
||||
phase: "during-merge",
|
||||
}), { "Content-Type": "application/json" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("phase must be");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /workflow-steps/:id", () => {
|
||||
|
||||
@@ -5988,7 +5988,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, prompt, scriptName, enabled, modelProvider, modelId } = req.body;
|
||||
const { name, description, mode, phase, prompt, scriptName, enabled, modelProvider, modelId } = req.body;
|
||||
|
||||
if (!name || typeof name !== "string" || !name.trim()) {
|
||||
res.status(400).json({ error: "name is required" });
|
||||
@@ -6006,6 +6006,12 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate phase
|
||||
if (phase !== undefined && phase !== "pre-merge" && phase !== "post-merge") {
|
||||
res.status(400).json({ error: "phase must be 'pre-merge' or 'post-merge'" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt !== undefined && typeof prompt !== "string") {
|
||||
res.status(400).json({ error: "prompt must be a string" });
|
||||
return;
|
||||
@@ -6047,6 +6053,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
mode: resolvedMode,
|
||||
phase,
|
||||
prompt: prompt?.trim(),
|
||||
scriptName: scriptName?.trim(),
|
||||
enabled,
|
||||
@@ -6069,7 +6076,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, prompt, scriptName, enabled, modelProvider, modelId } = req.body;
|
||||
const { name, description, mode, phase, prompt, scriptName, enabled, modelProvider, modelId } = req.body;
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (name !== undefined) {
|
||||
@@ -6093,6 +6100,13 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
updates.mode = mode;
|
||||
}
|
||||
if (phase !== undefined) {
|
||||
if (phase !== "pre-merge" && phase !== "post-merge") {
|
||||
res.status(400).json({ error: "phase must be 'pre-merge' or 'post-merge'" });
|
||||
return;
|
||||
}
|
||||
updates.phase = phase;
|
||||
}
|
||||
if (prompt !== undefined) {
|
||||
if (typeof prompt !== "string") {
|
||||
res.status(400).json({ error: "prompt must be a string" });
|
||||
|
||||
Reference in New Issue
Block a user