@runfusion/fusion 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/bin.js +2254 -1349
  2. package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +18 -0
  3. package/dist/client/assets/{AgentsView-BkB9FiMT.js → AgentsView-B3jYk8Kt.js} +3 -3
  4. package/dist/client/assets/ChatView-DhPkiEGs.js +1 -0
  5. package/dist/client/assets/{DevServerView-BkvtjZBa.js → DevServerView-DyGDEiBP.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-BK-KbnhP.js → DirectoryPicker-D5UIeIl6.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-BEg1CQAk.js → DocumentsView-DNHu1T8K.js} +1 -1
  8. package/dist/client/assets/{EvalsView-Berf9bQm.js → EvalsView-CpRobtDi.js} +1 -1
  9. package/dist/client/assets/{ExperimentalAgentOnboardingModal-jcInE50G.js → ExperimentalAgentOnboardingModal-DOY_oZi7.js} +1 -1
  10. package/dist/client/assets/{InsightsView-BX5bSF1J.js → InsightsView-vp0RE8Mg.js} +1 -1
  11. package/dist/client/assets/MemoryView-PSc5lGJt.js +2 -0
  12. package/dist/client/assets/MemoryView-zaXewZzi.css +1 -0
  13. package/dist/client/assets/{NodesView-DLUOBLf6.js → NodesView-DMj6HGeC.js} +1 -1
  14. package/dist/client/assets/{PiExtensionsManager-COlJf0Kx.js → PiExtensionsManager-DL_QcN56.js} +2 -2
  15. package/dist/client/assets/PluginManager-BtYKm8IT.js +1 -0
  16. package/dist/client/assets/{ResearchView-B256Lr8I.js → ResearchView-BhWqfdV0.js} +1 -1
  17. package/dist/client/assets/{SettingsModal-BeA_nQtW.js → SettingsModal-BAgB4_AR.js} +4 -4
  18. package/dist/client/assets/{SettingsModal-yRqM4DV8.js → SettingsModal-CUCyaAyE.js} +1 -1
  19. package/dist/client/assets/{SetupWizardModal-uUZk3TKT.js → SetupWizardModal-BKscasuh.js} +1 -1
  20. package/dist/client/assets/{SkillsView-CP8JX0P_.js → SkillsView-BdELqTy7.js} +1 -1
  21. package/dist/client/assets/{TodoView-DCRIkDZ-.js → TodoView-DFNGBDNV.js} +1 -1
  22. package/dist/client/assets/{folder-open-DHjELt8-.js → folder-open-k1xmUMyr.js} +1 -1
  23. package/dist/client/assets/index-Qq2JOOWx.css +1 -0
  24. package/dist/client/assets/{index-CQyVRLOb.js → index-TFYXEVpn.js} +160 -160
  25. package/dist/client/assets/{star-DYesq1AV.js → star-ne32r3Y4.js} +1 -1
  26. package/dist/client/assets/{upload-DTWF3Db5.js → upload-MS-2Gx53.js} +1 -1
  27. package/dist/client/assets/{users--syrel4l.js → users-C519GSjH.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/droid-cli/package.json +1 -1
  31. package/dist/extension.js +1370 -629
  32. package/dist/pi-claude-cli/package.json +1 -1
  33. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +9 -11
  34. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  35. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +30 -0
  36. package/dist/plugins/fusion-plugin-dependency-graph/package.json +3 -28
  37. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +899 -895
  38. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  39. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +68 -71
  40. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  41. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +47 -50
  42. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  43. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +155 -109
  44. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  45. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  46. package/dist/plugins/fusion-plugin-reports/src/index.ts +49 -3
  47. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +38 -0
  48. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-schema.test.ts +66 -0
  49. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-store.test.ts +177 -0
  50. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +341 -0
  51. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +77 -0
  52. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  53. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  54. package/package.json +1 -1
  55. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +0 -18
  56. package/dist/client/assets/ChatView-B_-B8fqu.js +0 -1
  57. package/dist/client/assets/MemoryView-CKElJY_3.js +0 -2
  58. package/dist/client/assets/MemoryView-DiajLXby.css +0 -1
  59. package/dist/client/assets/PluginManager-CfW55BF4.js +0 -1
  60. package/dist/client/assets/createLucideIcon-BazL2hk5.js +0 -21
  61. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  62. package/dist/client/assets/dashboard-view-CyWN-d02.js +0 -63
  63. package/dist/client/assets/dashboard-view-DdGlfuu-.css +0 -1
  64. package/dist/client/assets/dashboard-view-lR7YYmSC.js +0 -21
  65. package/dist/client/assets/index-CxA2Nn0_.css +0 -1
  66. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +0 -58
  67. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +0 -301
  68. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +0 -27
  69. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +0 -157
  70. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +0 -126
  71. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +0 -35
  72. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +0 -36
  73. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +0 -112
  74. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +0 -115
  75. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +0 -128
  76. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +0 -82
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +0 -307
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +0 -60
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +0 -75
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +0 -62
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +0 -78
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +0 -95
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +0 -74
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +0 -58
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +0 -121
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +0 -70
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +0 -89
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +0 -86
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +0 -167
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +0 -66
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +0 -81
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +0 -35
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +0 -19
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +0 -70
  95. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +0 -8
  96. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +0 -53
  97. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +0 -60
  98. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +0 -45
  99. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +0 -114
  100. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +0 -24
  101. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +0 -91
  102. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +0 -15
  103. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +0 -21
  104. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +0 -17
  105. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +0 -292
  106. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +0 -65
@@ -1,74 +0,0 @@
1
- import { createElement } from "react";
2
- import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
- import { describe, expect, it, vi, afterEach } from "vitest";
4
- import { definePlugin } from "@fusion/plugin-sdk";
5
- import { validatePluginManifest } from "@fusion/core";
6
- import plugin from "../index";
7
- import { DependencyGraphDashboardView } from "../dashboard-view";
8
- import { getPluginViewId } from "../../../../packages/dashboard/app/plugins/pluginViewRegistry";
9
-
10
- vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
11
- TaskCard: ({ task, onOpenDetail }: { task: { id: string }; onOpenDetail: (task: { id: string }) => void }) =>
12
- createElement("button", { "data-testid": `task-${task.id}`, onClick: () => onOpenDetail(task) }, task.id),
13
- }));
14
-
15
- afterEach(() => {
16
- cleanup();
17
- });
18
-
19
- describe("dependency graph plugin host integration contract", () => {
20
- it("declares dashboard view manifest shape", () => {
21
- expect(plugin.manifest.id).toBe("fusion-plugin-dependency-graph");
22
- expect(plugin.dashboardViews).toEqual([
23
- expect.objectContaining({
24
- viewId: "graph",
25
- label: "Graph",
26
- componentPath: "./dashboard-view",
27
- placement: "more",
28
- }),
29
- ]);
30
- });
31
-
32
- it("is valid for definePlugin + manifest validation", () => {
33
- const defined = definePlugin(plugin);
34
- const validation = validatePluginManifest(defined.manifest);
35
-
36
- expect(validation.valid).toBe(true);
37
- expect(validation.errors).toEqual([]);
38
- });
39
-
40
- it("produces loader-compatible pluginId/view entries", () => {
41
- const entries = (plugin.dashboardViews ?? []).map((view) => ({ pluginId: plugin.manifest.id, view }));
42
-
43
- expect(entries).toHaveLength(1);
44
- expect(entries[0]).toEqual(
45
- expect.objectContaining({
46
- pluginId: "fusion-plugin-dependency-graph",
47
- view: expect.objectContaining({ viewId: "graph" }),
48
- }),
49
- );
50
- });
51
-
52
- it("matches host registry lookup key format plugin:{pluginId}:{viewId}", () => {
53
- const view = plugin.dashboardViews?.[0];
54
- if (!view) throw new Error("missing dashboard view");
55
-
56
- expect(getPluginViewId(plugin.manifest.id, view.viewId)).toBe("plugin:fusion-plugin-dependency-graph:graph");
57
- });
58
-
59
- it("uses host openTaskDetail context when rendered through dashboard view entrypoint", () => {
60
- const openTaskDetail = vi.fn();
61
- render(
62
- createElement(DependencyGraphDashboardView, {
63
- context: {
64
- tasks: [{ id: "FN-HOST", description: "FN-HOST", column: "todo", dependencies: [], steps: [], currentStep: 0, log: [] }],
65
- openTaskDetail,
66
- } as never,
67
- }),
68
- );
69
-
70
- fireEvent.click(screen.getByTestId("task-FN-HOST"));
71
- expect(openTaskDetail).toHaveBeenCalledTimes(1);
72
- expect(openTaskDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-HOST" }));
73
- });
74
- });
@@ -1,58 +0,0 @@
1
- import { mkdtempSync } from "node:fs";
2
- import { rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { PluginLoader, PluginStore } from "@fusion/core";
7
- import { afterEach, describe, expect, it } from "vitest";
8
- import plugin from "../index";
9
-
10
- const testDirs: string[] = [];
11
-
12
- afterEach(async () => {
13
- await Promise.all(testDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
14
- });
15
-
16
- describe("dependency graph plugin index", () => {
17
- it("exports node-importable plugin metadata", () => {
18
- expect(plugin).toBeDefined();
19
- expect(plugin.manifest.id).toBe("fusion-plugin-dependency-graph");
20
- expect(plugin.dashboardViews?.[0]).toEqual(
21
- expect.objectContaining({
22
- viewId: "graph",
23
- componentPath: "./dashboard-view",
24
- }),
25
- );
26
- });
27
-
28
- it("loads src/index.ts via Node dynamic import", async () => {
29
- const moduleUrl = pathToFileURL(join(process.cwd(), "src/index.ts")).href;
30
- const module = await import(moduleUrl);
31
- expect(module.default?.manifest?.id).toBe("fusion-plugin-dependency-graph");
32
- });
33
-
34
- it("is loadable by PluginLoader without throwing", async () => {
35
- const rootDir = mkdtempSync(join(tmpdir(), "fn-3737-plugin-loader-"));
36
- testDirs.push(rootDir);
37
-
38
- const pluginStore = new PluginStore(rootDir, { inMemoryDb: true, centralGlobalDir: rootDir });
39
- await pluginStore.init();
40
-
41
- const pluginPath = join(process.cwd(), "src/index.ts");
42
- await pluginStore.registerPlugin({ manifest: plugin.manifest, path: pluginPath });
43
-
44
- const loader = new PluginLoader({
45
- pluginStore,
46
- taskStore: { logActivity: async () => undefined } as never,
47
- pluginDirs: [dirname(dirname(pluginPath))],
48
- });
49
-
50
- await loader.loadPlugin(plugin.manifest.id);
51
-
52
- const loaded = loader.getPlugin(plugin.manifest.id);
53
- expect(loaded?.state).toBe("started");
54
- expect(loaded?.dashboardViews?.[0]).toEqual(
55
- expect.objectContaining({ viewId: "graph", componentPath: "./dashboard-view" }),
56
- );
57
- });
58
- });
@@ -1,121 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { act, cleanup, fireEvent, render, renderHook, screen } from "@testing-library/react";
3
- import type { Task } from "@fusion/core";
4
- import { DependencyGraph } from "../DependencyGraph";
5
- import { GraphTaskNode } from "../GraphTaskNode";
6
- import { useGraphInteraction } from "../useGraphInteraction";
7
-
8
- vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
9
- TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => (
10
- <button data-testid={`task-${task.id}`} onClick={() => onOpenDetail(task)}>
11
- {task.id} {task.column === "in-progress" ? "Executing" : "Idle"}
12
- </button>
13
- ),
14
- }));
15
-
16
- function createTask(id: string, column: Task["column"] = "todo", dependencies: string[] = []): Task {
17
- return {
18
- id,
19
- description: id,
20
- column,
21
- dependencies,
22
- steps: [{ name: "one", status: "in-progress" }],
23
- currentStep: 0,
24
- status: column === "in-progress" ? "executing" : "queued",
25
- log: [],
26
- } as Task;
27
- }
28
-
29
- afterEach(() => {
30
- cleanup();
31
- });
32
-
33
- describe("dependency graph interactions", () => {
34
- it("supports pan and zoom via interaction hook", () => {
35
- const { result } = renderHook(() => useGraphInteraction());
36
-
37
- act(() => {
38
- result.current.onPointerDown(1, { x: 10, y: 10 });
39
- result.current.onPointerMove(1, { x: 110, y: 60 }, 800, 600);
40
- result.current.zoomIn();
41
- });
42
-
43
- expect(result.current.pan).toEqual({ x: 100, y: 50 });
44
- expect(result.current.zoom).toBeGreaterThan(1);
45
- });
46
-
47
- it("fit-to-graph computes bounds from actual node positions", () => {
48
- const { result } = renderHook(() => useGraphInteraction());
49
- const positions = new Map([
50
- ["A", { x: 0, y: 0 }],
51
- ["B", { x: 1000, y: 400 }],
52
- ]);
53
-
54
- act(() => {
55
- result.current.fitToGraph(positions, 800, 600, { nodeWidth: 200, nodeHeight: 100, xGap: 40, yGap: 40 });
56
- });
57
-
58
- expect(result.current.zoom).toBeCloseTo(0.6, 3);
59
- expect(result.current.pan.x).toBeCloseTo(40, 3);
60
- expect(result.current.pan.y).toBeCloseTo(150, 3);
61
- });
62
-
63
- it("clicking a node opens task detail", () => {
64
- const onOpenDetail = vi.fn();
65
- render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenDetail={onOpenDetail} />);
66
-
67
- fireEvent.click(screen.getByTestId("task-A"));
68
- expect(onOpenDetail).toHaveBeenCalledTimes(1);
69
- expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "A" }));
70
- expect(screen.getAllByText(/Executing/).length).toBeGreaterThan(0);
71
- });
72
-
73
- it("dragging a node updates its position", () => {
74
- const onNodePositionChange = vi.fn();
75
-
76
- render(
77
- <GraphTaskNode
78
- task={createTask("A")}
79
- position={{ x: 0, y: 0 }}
80
- scale={1}
81
- isHighlighted={false}
82
- isDimmed={false}
83
- onNodePositionChange={onNodePositionChange}
84
- onNodeDragStateChange={vi.fn()}
85
- projectId="p1"
86
- onOpenDetail={vi.fn()}
87
- addToast={vi.fn()}
88
- onUpdateTask={vi.fn()}
89
- onArchiveTask={vi.fn()}
90
- onUnarchiveTask={vi.fn()}
91
- onDeleteTask={vi.fn()}
92
- onRetryTask={vi.fn()}
93
- onOpenDetailWithTab={vi.fn()}
94
- onMoveTask={vi.fn()}
95
- onOpenMission={vi.fn()}
96
- taskStuckTimeoutMs={1_000}
97
- lastFetchTimeMs={Date.now()}
98
- workflowStepNameLookup={new Map()}
99
- />,
100
- );
101
-
102
- const node = screen.getByTestId("graph-task-node-A");
103
- fireEvent.pointerDown(node, { pointerId: 1, clientX: 10, clientY: 10, isPrimary: true });
104
- fireEvent.pointerMove(node, { pointerId: 1, clientX: 25, clientY: 30, isPrimary: true });
105
-
106
- expect(onNodePositionChange).toHaveBeenCalled();
107
- });
108
-
109
- it("highlights upstream/downstream chain on hover", () => {
110
- render(
111
- <DependencyGraph
112
- tasks={[createTask("A"), createTask("B", "todo", ["A"]), createTask("C", "todo", ["B"]), createTask("D")]}
113
- onOpenDetail={vi.fn()}
114
- />,
115
- );
116
-
117
- fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
118
- expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
119
- expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--dimmed");
120
- });
121
- });
@@ -1,70 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { GraphData } from "../types";
3
- import { computeAutoLayout } from "../layout";
4
-
5
- function graph(nodeIds: string[], edges: Array<{ source: string; target: string }> = []): GraphData {
6
- return {
7
- nodes: nodeIds.map((id) => ({ task: { id } as never })),
8
- edges,
9
- };
10
- }
11
-
12
- describe("computeAutoLayout", () => {
13
- it("returns empty map for empty graph", () => {
14
- expect(computeAutoLayout({ nodes: [], edges: [] }).size).toBe(0);
15
- });
16
-
17
- it("positions single node", () => {
18
- const positions = computeAutoLayout(graph(["A"]));
19
- expect(positions.has("A")).toBe(true);
20
- });
21
-
22
- it("places linear chain in increasing depth", () => {
23
- const positions = computeAutoLayout(graph(["A", "B", "C"], [
24
- { source: "A", target: "B" },
25
- { source: "B", target: "C" },
26
- ]));
27
-
28
- expect((positions.get("C")?.y ?? 0)).toBeLessThan(positions.get("B")?.y ?? 0);
29
- expect((positions.get("B")?.y ?? 0)).toBeLessThan(positions.get("A")?.y ?? 0);
30
- });
31
-
32
- it("spreads wide layer horizontally", () => {
33
- const positions = computeAutoLayout(graph(["A", "B", "C"]));
34
- const xs = [positions.get("A")?.x, positions.get("B")?.x, positions.get("C")?.x].filter((x): x is number => x !== undefined);
35
- expect(new Set(xs).size).toBe(3);
36
- });
37
-
38
- it("handles diamond dependencies", () => {
39
- const positions = computeAutoLayout(graph(["A", "B", "C", "D"], [
40
- { source: "A", target: "B" },
41
- { source: "A", target: "C" },
42
- { source: "B", target: "D" },
43
- { source: "C", target: "D" },
44
- ]));
45
-
46
- expect((positions.get("D")?.y ?? 0)).toBeLessThan(positions.get("B")?.y ?? 0);
47
- expect((positions.get("D")?.y ?? 0)).toBeLessThan(positions.get("C")?.y ?? 0);
48
- expect((positions.get("B")?.y ?? 0)).toBeLessThan(positions.get("A")?.y ?? 0);
49
- expect((positions.get("C")?.y ?? 0)).toBeLessThan(positions.get("A")?.y ?? 0);
50
- });
51
-
52
- it("handles cycles without crashing", () => {
53
- const positions = computeAutoLayout(graph(["A", "B"], [
54
- { source: "A", target: "B" },
55
- { source: "B", target: "A" },
56
- ]));
57
-
58
- expect(positions.size).toBe(2);
59
- });
60
-
61
- it("respects custom spacing options", () => {
62
- const positions = computeAutoLayout(graph(["A", "B"]), {
63
- nodeWidth: 200,
64
- nodeHeight: 120,
65
- horizontalGap: 100,
66
- verticalGap: 20,
67
- });
68
- expect(Math.abs((positions.get("A")?.x ?? 0) - (positions.get("B")?.x ?? 0))).toBe(300);
69
- });
70
- });
@@ -1,89 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
- import type { Task } from "@fusion/core";
4
- import { DependencyGraph } from "../DependencyGraph";
5
- import { loadPositions, savePositions } from "../utils/graphPositionStorage";
6
-
7
- vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
8
- TaskCard: ({ task }: { task: Task }) => <div>{task.id}</div>,
9
- }));
10
-
11
- function createStorage() {
12
- const store = new Map<string, string>();
13
- return {
14
- getItem: (key: string) => store.get(key) ?? null,
15
- setItem: (key: string, value: string) => {
16
- store.set(key, value);
17
- },
18
- removeItem: (key: string) => {
19
- store.delete(key);
20
- },
21
- };
22
- }
23
-
24
- function createTask(id: string, column: Task["column"] = "todo"): Task {
25
- return { id, description: id, column, dependencies: [], steps: [], currentStep: 0, log: [] } as Task;
26
- }
27
-
28
- describe("dependency graph position persistence", () => {
29
- beforeEach(() => {
30
- Object.defineProperty(window, "localStorage", { value: createStorage(), configurable: true });
31
- });
32
-
33
- afterEach(() => {
34
- cleanup();
35
- vi.restoreAllMocks();
36
- });
37
-
38
- it("saves/restores project-scoped position shape", () => {
39
- savePositions({ A: { x: 10, y: 20 }, B: { x: 30, y: 40 } }, new Set(["A", "B"]), "p1");
40
-
41
- expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBe(
42
- JSON.stringify({ A: { x: 10, y: 20 }, B: { x: 30, y: 40 } }),
43
- );
44
- expect(loadPositions("p1")).toEqual({ A: { x: 10, y: 20 }, B: { x: 30, y: 40 } });
45
- });
46
-
47
- it("keeps positions isolated across projects", () => {
48
- savePositions({ A: { x: 1, y: 2 } }, new Set(["A"]), "p1");
49
- savePositions({ A: { x: 99, y: 88 } }, new Set(["A"]), "p2");
50
-
51
- expect(loadPositions("p1")).toEqual({ A: { x: 1, y: 2 } });
52
- expect(loadPositions("p2")).toEqual({ A: { x: 99, y: 88 } });
53
- });
54
-
55
- it("falls back to auto-layout with corrupt storage and does not crash", () => {
56
- window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", "{broken");
57
-
58
- render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
59
- expect(screen.getByTestId("graph-task-node-A")).toBeTruthy();
60
- });
61
-
62
- it("drag persistence only writes localStorage and performs no network writes", () => {
63
- const fetchSpy = vi.spyOn(globalThis, "fetch");
64
-
65
- render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
66
- const node = screen.getByTestId("graph-task-node-A");
67
- fireEvent.pointerDown(node, { pointerId: 1, isPrimary: true, clientX: 10, clientY: 10 });
68
- fireEvent.pointerMove(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 });
69
- fireEvent.pointerUp(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 });
70
-
71
- expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toContain('"A"');
72
- expect(fetchSpy).not.toHaveBeenCalled();
73
- });
74
-
75
- it("clearing localStorage causes fresh auto-layout on remount", () => {
76
- const { unmount } = render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
77
- const node = screen.getByTestId("graph-task-node-A");
78
- fireEvent.pointerDown(node, { pointerId: 1, isPrimary: true, clientX: 10, clientY: 10 });
79
- fireEvent.pointerMove(node, { pointerId: 1, isPrimary: true, clientX: 40, clientY: 50 });
80
- fireEvent.pointerUp(node, { pointerId: 1, isPrimary: true, clientX: 40, clientY: 50 });
81
-
82
- window.localStorage.removeItem("kb:p1:fusion-plugin-dependency-graph:positions");
83
- unmount();
84
-
85
- render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
86
- expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBeNull();
87
- expect(screen.getByTestId("graph-task-node-A")).toBeTruthy();
88
- });
89
- });
@@ -1,86 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { renderHook } from "@testing-library/react";
3
- import type { Task } from "@fusion/core";
4
- import { useGraphData } from "../useGraphData";
5
-
6
- function createTask(id: string, column: Task["column"] = "todo", dependencies: string[] = []): Task {
7
- return {
8
- id,
9
- description: id,
10
- column,
11
- dependencies,
12
- steps: [],
13
- currentStep: 0,
14
- log: [],
15
- } as Task;
16
- }
17
-
18
- describe("useGraphData", () => {
19
- it("returns empty graph for empty tasks", () => {
20
- const { result } = renderHook(() => useGraphData([]));
21
- expect(result.current).toEqual({ nodes: [], edges: [] });
22
- });
23
-
24
- it("creates node for single task with no deps", () => {
25
- const { result } = renderHook(() => useGraphData([createTask("A")]));
26
- expect(result.current.nodes.map((node) => node.task.id)).toEqual(["A"]);
27
- expect(result.current.edges).toEqual([]);
28
- });
29
-
30
- describe("orphan dependencies to excluded tasks", () => {
31
- it("drops dependency edge to done task while keeping dependent node", () => {
32
- const filteredTasks = [createTask("A", "in-progress", ["DONE-1"])];
33
- const { result } = renderHook(() => useGraphData(filteredTasks));
34
-
35
- expect(result.current.nodes.map((node) => node.task.id)).toEqual(["A"]);
36
- expect(result.current.edges).toEqual([]);
37
- });
38
-
39
- it("drops dependency edge to archived task while keeping dependent node", () => {
40
- const filteredTasks = [createTask("A", "triage", ["ARCH-1"])];
41
- const { result } = renderHook(() => useGraphData(filteredTasks));
42
-
43
- expect(result.current.nodes.map((node) => node.task.id)).toEqual(["A"]);
44
- expect(result.current.edges).toEqual([]);
45
- });
46
-
47
- it("keeps only included dependency edges when mixed dependencies are present", () => {
48
- const filteredTasks = [createTask("A", "in-progress", ["B", "DONE-1", "ARCH-1"]), createTask("B", "todo")];
49
- const { result } = renderHook(() => useGraphData(filteredTasks));
50
-
51
- expect(result.current.nodes.map((node) => node.task.id)).toEqual(["A", "B"]);
52
- expect(result.current.edges).toEqual([{ source: "A", target: "B" }]);
53
- });
54
-
55
- it("shows zero edges when all dependencies are excluded", () => {
56
- const filteredTasks = [createTask("A", "in-progress", ["DONE-1", "ARCH-1"])];
57
- const { result } = renderHook(() => useGraphData(filteredTasks));
58
-
59
- expect(result.current.nodes.map((node) => node.task.id)).toEqual(["A"]);
60
- expect(result.current.edges).toEqual([]);
61
- });
62
- });
63
-
64
- describe("in-review dependency edges", () => {
65
- it("renders edges between in-review tasks", () => {
66
- const tasks = [createTask("A", "in-review", ["B"]), createTask("B", "in-review")];
67
- const { result } = renderHook(() => useGraphData(tasks));
68
-
69
- expect(result.current.edges).toEqual([{ source: "A", target: "B" }]);
70
- });
71
-
72
- it("renders edge from in-review task to in-progress task", () => {
73
- const tasks = [createTask("A", "in-review", ["B"]), createTask("B", "in-progress")];
74
- const { result } = renderHook(() => useGraphData(tasks));
75
-
76
- expect(result.current.edges).toEqual([{ source: "A", target: "B" }]);
77
- });
78
-
79
- it("renders edge from in-progress task to in-review task", () => {
80
- const tasks = [createTask("A", "in-progress", ["B"]), createTask("B", "in-review")];
81
- const { result } = renderHook(() => useGraphData(tasks));
82
-
83
- expect(result.current.edges).toEqual([{ source: "A", target: "B" }]);
84
- });
85
- });
86
- });
@@ -1,167 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { act, renderHook } from "@testing-library/react";
3
- import type React from "react";
4
- import { useGraphInteraction } from "../useGraphInteraction";
5
-
6
- function createKeyEvent(
7
- key: string,
8
- options?: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; target?: EventTarget | null },
9
- ) {
10
- return {
11
- key,
12
- ctrlKey: Boolean(options?.ctrlKey),
13
- metaKey: Boolean(options?.metaKey),
14
- shiftKey: Boolean(options?.shiftKey),
15
- target: options?.target ?? document.createElement("div"),
16
- preventDefault: vi.fn(),
17
- } as unknown as React.KeyboardEvent;
18
- }
19
-
20
- describe("useGraphInteraction", () => {
21
- it("starts with default pan/zoom", () => {
22
- const { result } = renderHook(() => useGraphInteraction());
23
- expect(result.current.zoom).toBe(1);
24
- expect(result.current.zoomPercent).toBe(100);
25
- expect(result.current.pan).toEqual({ x: 0, y: 0 });
26
- });
27
-
28
- it("clamps zoom between 0.1 and 3", () => {
29
- const { result } = renderHook(() => useGraphInteraction());
30
-
31
- act(() => {
32
- for (let i = 0; i < 100; i += 1) result.current.zoomOut();
33
- });
34
- expect(result.current.zoom).toBe(0.1);
35
-
36
- act(() => {
37
- for (let i = 0; i < 100; i += 1) result.current.zoomIn();
38
- });
39
- expect(result.current.zoom).toBe(3);
40
- });
41
-
42
- it("keeps wheel zoom anchored to cursor position", () => {
43
- const { result } = renderHook(() => useGraphInteraction());
44
-
45
- act(() => {
46
- result.current.onWheelZoom(-120, { x: 200, y: 150 }, 800, 600);
47
- });
48
-
49
- expect(result.current.zoom).toBe(1.1);
50
- expect(result.current.pan.x).toBeCloseTo(-20, 5);
51
- expect(result.current.pan.y).toBeCloseTo(-15, 5);
52
- });
53
-
54
- it("supports pinch zoom with stationary midpoint", () => {
55
- const { result } = renderHook(() => useGraphInteraction());
56
-
57
- act(() => {
58
- result.current.onPointerDown(1, { x: 100, y: 100 });
59
- result.current.onPointerDown(2, { x: 200, y: 100 });
60
- result.current.onPointerMove(2, { x: 250, y: 100 }, 800, 600);
61
- });
62
-
63
- expect(result.current.zoom).toBe(1.5);
64
- expect(result.current.pan).toEqual({ x: -50, y: -50 });
65
- });
66
-
67
- it("applies animation state for fit and reset", () => {
68
- vi.useFakeTimers();
69
- const { result } = renderHook(() => useGraphInteraction());
70
-
71
- act(() => {
72
- result.current.fitToGraph(new Map([["A", { x: 0, y: 0 }]]), 800, 600);
73
- });
74
- expect(result.current.transitioning).toBe(true);
75
-
76
- act(() => {
77
- vi.advanceTimersByTime(210);
78
- });
79
- expect(result.current.transitioning).toBe(false);
80
-
81
- act(() => {
82
- result.current.resetView();
83
- });
84
- expect(result.current.transitioning).toBe(true);
85
-
86
- act(() => {
87
- vi.advanceTimersByTime(210);
88
- });
89
- expect(result.current.transitioning).toBe(false);
90
- vi.useRealTimers();
91
- });
92
-
93
- it("fits wide graph", () => {
94
- const { result } = renderHook(() => useGraphInteraction());
95
- act(() => {
96
- result.current.fitToGraph(new Map([
97
- ["A", { x: 0, y: 0 }],
98
- ["B", { x: 2000, y: 0 }],
99
- ]), 800, 600);
100
- });
101
-
102
- expect(result.current.zoom).toBeLessThan(1);
103
- });
104
-
105
- it("handles keyboard shortcuts for zoom in/out, reset, fit, and escape", () => {
106
- const { result } = renderHook(() => useGraphInteraction());
107
- const positions = new Map([
108
- ["A", { x: 0, y: 0 }],
109
- ["B", { x: 500, y: 200 }],
110
- ]);
111
-
112
- act(() => {
113
- result.current.handleKeyDown(createKeyEvent("=", { ctrlKey: true }), 800, 600, positions);
114
- });
115
- expect(result.current.zoom).toBe(1.2);
116
-
117
- act(() => {
118
- result.current.handleKeyDown(createKeyEvent("-", { ctrlKey: true }), 800, 600, positions);
119
- });
120
- expect(result.current.zoom).toBeCloseTo(1, 5);
121
-
122
- act(() => {
123
- result.current.handleKeyDown(createKeyEvent("F", { ctrlKey: true, shiftKey: true }), 800, 600, positions, { nodeWidth: 280, nodeHeight: 100 });
124
- });
125
- expect(result.current.zoom).toBeLessThan(1);
126
-
127
- act(() => {
128
- result.current.handleKeyDown(createKeyEvent("0", { ctrlKey: true }), 800, 600, positions);
129
- });
130
- expect(result.current.zoom).toBe(1);
131
- expect(result.current.pan).toEqual({ x: 0, y: 0 });
132
-
133
- act(() => {
134
- result.current.zoomIn();
135
- result.current.handleKeyDown(createKeyEvent("Escape"), 800, 600, positions);
136
- });
137
- expect(result.current.zoom).toBe(1);
138
- expect(result.current.pan).toEqual({ x: 0, y: 0 });
139
- });
140
-
141
- it("does not run shortcuts when focused on editable targets", () => {
142
- const { result } = renderHook(() => useGraphInteraction());
143
- const input = document.createElement("input");
144
- const positions = new Map([["A", { x: 0, y: 0 }]]);
145
-
146
- act(() => {
147
- result.current.handleKeyDown(createKeyEvent("=", { ctrlKey: true, target: input }), 800, 600, positions);
148
- });
149
-
150
- expect(result.current.zoom).toBe(1);
151
- });
152
-
153
- it("resets when positions are empty", () => {
154
- const { result } = renderHook(() => useGraphInteraction());
155
-
156
- act(() => {
157
- result.current.zoomIn();
158
- result.current.onPointerDown(1, { x: 10, y: 10 });
159
- result.current.onPointerMove(1, { x: 110, y: 60 }, 800, 600);
160
- result.current.onPointerUp(1);
161
- result.current.fitToGraph(new Map(), 800, 600);
162
- });
163
-
164
- expect(result.current.zoom).toBe(1);
165
- expect(result.current.pan).toEqual({ x: 0, y: 0 });
166
- });
167
- });