Greptile + CodeRabbit findings across core/engine/dashboard. Stale findings (written against earlier commits) verified and skipped; valid ones fixed. Engine: - await-input: do not clear pausedReason in the /input route (the node's marker must survive unpause); the node clears it after consuming input. Embed a colon-free epoch watermark in the marker so only post-pause steering comments count as the reply (ISO timestamps collided with the colon separator and the dashboard question parser). - gate nodes without a registered runner now fail closed (throw) instead of silently passing. - a thrown interpreter error in maybeExecuteWorkflowGraph now falls back to the legacy pipeline instead of stranding the task in-progress. - approved-CLI path clears the stale awaiting-cli-approval status/marker. Core: - persist+cascade workflow selection: purge task_workflow_selection rows and compiled workflow_steps on physical task deletes; migration 105 cleans already-orphaned rows; catch-cleanup for materialized steps when the owner write fails; WF-id allocation now in a BEGIN IMMEDIATE transaction. - compiler validates the canonical execute->review->merge seam order (rejects duplicate/misordered seams). - disk-backed reopen round-trip + tightened updatedAt/list assertions. Dashboard: - WorkflowSelector clears stale default/options across project changes and on fetch failure; InlineCreateCard/NewTaskModal reset the workflow on all clear/discard paths and include it in dirty-state. - WorkflowNodeEditor: config-key deletion now persists; removed an invalid eslint-disable that was itself a hard lint error. - TaskCard: single status badge for awaiting-input (no duplicate). - WorkflowResultsTab: reset paused-action UI between pauses; surface resume/approve failures inline. - TaskDetailModal: treat awaiting-user-input/awaiting-cli-approval/paused as not-in-progress for the live-log subscription. - workflow-flow-mapping: don't write synthetic node names back into IR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { WorkflowDefinition } from "@fusion/core";
|
|
import { irToFlow, flowToIr } from "../workflow-flow-mapping";
|
|
|
|
function makeDef(ir: WorkflowDefinition["ir"]): WorkflowDefinition {
|
|
return {
|
|
id: "WF-001",
|
|
name: ir.name,
|
|
description: "",
|
|
ir,
|
|
layout: {},
|
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
describe("workflow-flow-mapping name preservation", () => {
|
|
it("does not inject synthetic names for unnamed start/end/merge nodes on round-trip", () => {
|
|
const ir: WorkflowDefinition["ir"] = {
|
|
version: "v1",
|
|
name: "wf",
|
|
nodes: [
|
|
{ id: "start", kind: "start" },
|
|
{ id: "n1", kind: "prompt", config: { prompt: "do work" } },
|
|
{ id: "m1", kind: "prompt", config: { seam: "merge" } },
|
|
{ id: "end", kind: "end" },
|
|
],
|
|
edges: [
|
|
{ from: "start", to: "n1", condition: "success" },
|
|
{ from: "n1", to: "m1", condition: "success" },
|
|
{ from: "m1", to: "end", condition: "success" },
|
|
],
|
|
};
|
|
|
|
const { nodes, edges } = irToFlow(makeDef(ir));
|
|
const { ir: out } = flowToIr("wf", nodes, edges);
|
|
|
|
const byId = Object.fromEntries(out.nodes.map((n) => [n.id, n]));
|
|
// start/end carry no config or a config without an injected name
|
|
expect(byId.start.config?.name).toBeUndefined();
|
|
expect(byId.end.config?.name).toBeUndefined();
|
|
// merge boundary keeps its seam but does not gain a synthetic "Merge boundary" name
|
|
expect(byId.m1.config?.seam).toBe("merge");
|
|
expect(byId.m1.config?.name).toBeUndefined();
|
|
// an unnamed prompt node keeps no synthetic id-as-name
|
|
expect(byId.n1.config?.name).toBeUndefined();
|
|
expect(byId.n1.config?.prompt).toBe("do work");
|
|
});
|
|
|
|
it("preserves an explicit node name across round-trips", () => {
|
|
const ir: WorkflowDefinition["ir"] = {
|
|
version: "v1",
|
|
name: "wf",
|
|
nodes: [
|
|
{ id: "start", kind: "start" },
|
|
{ id: "n1", kind: "prompt", config: { name: "Implement", prompt: "do work" } },
|
|
{ id: "end", kind: "end" },
|
|
],
|
|
edges: [
|
|
{ from: "start", to: "n1", condition: "success" },
|
|
{ from: "n1", to: "end", condition: "success" },
|
|
],
|
|
};
|
|
|
|
const { nodes, edges } = irToFlow(makeDef(ir));
|
|
const { ir: out } = flowToIr("wf", nodes, edges);
|
|
const n1 = out.nodes.find((n) => n.id === "n1");
|
|
expect(n1?.config?.name).toBe("Implement");
|
|
});
|
|
|
|
it("persists a user-entered label as the node name", () => {
|
|
const ir: WorkflowDefinition["ir"] = {
|
|
version: "v1",
|
|
name: "wf",
|
|
nodes: [
|
|
{ id: "start", kind: "start" },
|
|
{ id: "n1", kind: "prompt", config: { prompt: "do work" } },
|
|
{ id: "end", kind: "end" },
|
|
],
|
|
edges: [
|
|
{ from: "start", to: "n1", condition: "success" },
|
|
{ from: "n1", to: "end", condition: "success" },
|
|
],
|
|
};
|
|
|
|
const { nodes, edges } = irToFlow(makeDef(ir));
|
|
// Simulate the editor renaming the node via its label input.
|
|
const renamed = nodes.map((n) =>
|
|
n.id === "n1" ? { ...n, data: { ...n.data, label: "Build feature" } } : n,
|
|
);
|
|
const { ir: out } = flowToIr("wf", renamed, edges);
|
|
const n1 = out.nodes.find((n) => n.id === "n1");
|
|
expect(n1?.config?.name).toBe("Build feature");
|
|
});
|
|
});
|