import type { Edge as RFEdge, Node as RFNode } from "@xyflow/react"; import { describe, expect, it } from "vitest"; import type { Id } from "@/convex/_generated/dataModel"; import { reconcileCanvasFlowEdges, reconcileCanvasFlowNodes, } from "@/components/canvas/canvas-flow-reconciliation-helpers"; const asNodeId = (id: string): Id<"nodes"> => id as Id<"nodes">; const asEdgeId = (id: string) => id as Id<"edges">; describe("canvas flow reconciliation helpers", () => { it("carries an optimistic edge while a pending connection create awaits convex edge sync", () => { const previousEdges: RFEdge[] = [ { id: "optimistic_edge_req-1", source: "node-source", target: "optimistic_req-1", }, ]; const result = reconcileCanvasFlowEdges({ previousEdges, convexEdges: [], convexNodes: [ { _id: asNodeId("node-source"), type: "image" }, { _id: asNodeId("node-real"), type: "prompt" }, ], previousConvexNodeIdsSnapshot: new Set(["node-source"]), pendingRemovedEdgeIds: new Set(), pendingConnectionCreateIds: new Set(["req-1"]), resolvedRealIdByClientRequest: new Map(), localNodeIds: new Set(["node-source"]), isAnyNodeDragging: false, colorMode: "light", }); expect(result.edges).toEqual([ { id: "optimistic_edge_req-1", source: "node-source", target: "node-real", }, ]); expect(result.inferredRealIdByClientRequest).toEqual( new Map([["req-1", "node-real"]]), ); }); it("remaps optimistic endpoints to resolved real node ids", () => { const previousEdges: RFEdge[] = [ { id: "optimistic_edge_req-2", source: "optimistic_req-2", target: "node-target", }, ]; const result = reconcileCanvasFlowEdges({ previousEdges, convexEdges: [], convexNodes: [ { _id: asNodeId("node-real"), type: "image" }, { _id: asNodeId("node-target"), type: "prompt" }, ], previousConvexNodeIdsSnapshot: new Set(["node-real", "node-target"]), pendingRemovedEdgeIds: new Set(), pendingConnectionCreateIds: new Set(), resolvedRealIdByClientRequest: new Map([["req-2", asNodeId("node-real")]]), localNodeIds: new Set(["node-target"]), isAnyNodeDragging: false, colorMode: "light", }); expect(result.edges).toEqual([ { id: "optimistic_edge_req-2", source: "node-real", target: "node-target", }, ]); }); it("suppresses carried optimistic edges when convex already has the same connection signature", () => { const result = reconcileCanvasFlowEdges({ previousEdges: [ { id: "optimistic_edge_req-3", source: "optimistic_req-3", target: "node-target", }, ], convexEdges: [ { _id: asEdgeId("edge-1"), sourceNodeId: asNodeId("node-real"), targetNodeId: asNodeId("node-target"), sourceHandle: undefined, targetHandle: undefined, }, ], convexNodes: [ { _id: asNodeId("node-real"), type: "image" }, { _id: asNodeId("node-target"), type: "prompt" }, ], previousConvexNodeIdsSnapshot: new Set(["node-real", "node-target"]), pendingRemovedEdgeIds: new Set(), pendingConnectionCreateIds: new Set(), resolvedRealIdByClientRequest: new Map([["req-3", asNodeId("node-real")]]), localNodeIds: new Set(), isAnyNodeDragging: false, colorMode: "light", }); expect(result.edges).toHaveLength(1); expect(result.edges[0]).toMatchObject({ id: "edge-1", source: "node-real", target: "node-target", }); }); it("preserves temp edges while filtering pending removed convex edges", () => { const result = reconcileCanvasFlowEdges({ previousEdges: [ { id: "temp-edge-1", source: "node-a", target: "node-b", className: "temp", }, ], convexEdges: [ { _id: asEdgeId("edge-removed"), sourceNodeId: asNodeId("node-a"), targetNodeId: asNodeId("node-b"), sourceHandle: undefined, targetHandle: undefined, }, ], convexNodes: [ { _id: asNodeId("node-a"), type: "image" }, { _id: asNodeId("node-b"), type: "prompt" }, ], previousConvexNodeIdsSnapshot: new Set(["node-a", "node-b"]), pendingRemovedEdgeIds: new Set(["edge-removed"]), pendingConnectionCreateIds: new Set(), resolvedRealIdByClientRequest: new Map(), localNodeIds: new Set(), isAnyNodeDragging: false, colorMode: "light", }); expect(result.edges).toEqual([ { id: "temp-edge-1", source: "node-a", target: "node-b", className: "temp", }, ]); }); it("keeps optimistic endpoints for local dragging nodes and still carries the edge", () => { const result = reconcileCanvasFlowEdges({ previousEdges: [ { id: "optimistic_edge_req-drag", source: "node-source", target: "optimistic_req-drag", }, ], convexEdges: [], convexNodes: [{ _id: asNodeId("node-source"), type: "image" }], previousConvexNodeIdsSnapshot: new Set(["node-source"]), pendingRemovedEdgeIds: new Set(), pendingConnectionCreateIds: new Set(["req-drag"]), resolvedRealIdByClientRequest: new Map([["req-drag", asNodeId("node-real")]]), localNodeIds: new Set(["optimistic_req-drag"]), isAnyNodeDragging: true, colorMode: "light", }); expect(result.edges).toEqual([ { id: "optimistic_edge_req-drag", source: "node-source", target: "optimistic_req-drag", }, ]); }); it("reports settled pending connection creates once convex has the real node and edge", () => { const result = reconcileCanvasFlowEdges({ previousEdges: [], convexEdges: [ { _id: asEdgeId("edge-settled"), sourceNodeId: asNodeId("node-source"), targetNodeId: asNodeId("node-real"), sourceHandle: undefined, targetHandle: undefined, }, ], convexNodes: [ { _id: asNodeId("node-source"), type: "image" }, { _id: asNodeId("node-real"), type: "prompt" }, ], previousConvexNodeIdsSnapshot: new Set(["node-source", "node-real"]), pendingRemovedEdgeIds: new Set(), pendingConnectionCreateIds: new Set(["req-settled"]), resolvedRealIdByClientRequest: new Map([ ["req-settled", asNodeId("node-real")], ]), localNodeIds: new Set(), isAnyNodeDragging: false, colorMode: "light", }); expect(result.settledPendingConnectionCreateIds).toEqual(["req-settled"]); }); it("cleans up matched local position pins once convex catches up", () => { const previousNodes: RFNode[] = [ { id: "node-1", type: "image", position: { x: 120, y: 80 }, data: {}, }, ]; const incomingNodes: RFNode[] = [ { id: "node-1", type: "image", position: { x: 120, y: 80 }, data: {}, }, ]; const result = reconcileCanvasFlowNodes({ previousNodes, incomingNodes, convexNodes: [{ _id: asNodeId("node-1"), type: "image" }], deletingNodeIds: new Set(), resolvedRealIdByClientRequest: new Map(), pendingConnectionCreateIds: new Set(), preferLocalPositionNodeIds: new Set(), pendingLocalPositionPins: new Map([["node-1", { x: 120, y: 80 }]]), pendingMovePins: new Map(), }); expect(result.nodes).toEqual(incomingNodes); expect(result.nextPendingLocalPositionPins.size).toBe(0); }); it("filters deleting nodes from incoming reconciliation results", () => { const result = reconcileCanvasFlowNodes({ previousNodes: [ { id: "node-keep", type: "image", position: { x: 0, y: 0 }, data: {}, }, { id: "node-delete", type: "image", position: { x: 40, y: 40 }, data: {}, }, ], incomingNodes: [ { id: "node-keep", type: "image", position: { x: 0, y: 0 }, data: {}, }, { id: "node-delete", type: "image", position: { x: 40, y: 40 }, data: {}, }, ], convexNodes: [ { _id: asNodeId("node-keep"), type: "image" }, { _id: asNodeId("node-delete"), type: "image" }, ], deletingNodeIds: new Set(["node-delete"]), resolvedRealIdByClientRequest: new Map(), pendingConnectionCreateIds: new Set(), preferLocalPositionNodeIds: new Set(), pendingLocalPositionPins: new Map(), pendingMovePins: new Map(), }); expect(result.nodes).toEqual([ { id: "node-keep", type: "image", position: { x: 0, y: 0 }, data: {}, }, ]); }); });