Files
fusion/packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts
gsxdsm eb67d08213 Address PR review feedback (#1363)
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>
2026-06-03 17:32:12 -07:00

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");
});
});