@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,307 +0,0 @@
1
- import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2
- import type { Task } from "@fusion/core";
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
- import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
5
- import { GraphTaskNode } from "../GraphTaskNode";
6
-
7
- function createTask(overrides: Partial<Task> = {}): Task {
8
- return {
9
- id: "FN-TEST",
10
- description: "Task description",
11
- column: "todo",
12
- dependencies: [],
13
- steps: [],
14
- currentStep: 0,
15
- log: [],
16
- ...overrides,
17
- } as Task;
18
- }
19
-
20
- function createProps(task: Task) {
21
- return {
22
- task,
23
- position: { x: 0, y: 0 },
24
- scale: 1,
25
- onNodePositionChange: vi.fn(),
26
- onNodeDragStateChange: vi.fn(),
27
- projectId: "proj-1",
28
- onOpenDetail: vi.fn(),
29
- addToast: vi.fn(),
30
- onUpdateTask: vi.fn(),
31
- onArchiveTask: vi.fn(),
32
- onUnarchiveTask: vi.fn(),
33
- onDeleteTask: vi.fn(),
34
- onRetryTask: vi.fn(),
35
- onOpenDetailWithTab: vi.fn(),
36
- onMoveTask: vi.fn(),
37
- onOpenMission: vi.fn(),
38
- taskStuckTimeoutMs: 60_000,
39
- lastFetchTimeMs: Date.now(),
40
- workflowStepNameLookup: new Map<string, string>(),
41
- };
42
- }
43
-
44
- afterEach(() => {
45
- cleanup();
46
- });
47
-
48
- describe("GraphTaskNode", () => {
49
- it("renders a TaskCard and passes core props through", () => {
50
- const props = createProps(createTask());
51
- const { container } = render(<GraphTaskNode {...props} style={{ left: 10, top: 20 }} />);
52
-
53
- const node = screen.getByTestId("graph-task-node-FN-TEST");
54
- expect(node).toBeTruthy();
55
- expect(container.querySelector(".card-title")?.textContent).toContain("Task description");
56
- expect(node.getAttribute("draggable")).toBe("false");
57
- expect(container.querySelector(".card")?.getAttribute("draggable")).toBe("false");
58
- });
59
-
60
- it("shows active indicator with capitalized status for in-progress executing tasks", () => {
61
- const props = createProps(
62
- createTask({
63
- column: "in-progress",
64
- status: "executing",
65
- steps: [{ name: "step one", status: "in-progress" }],
66
- }),
67
- );
68
-
69
- const { container } = render(<GraphTaskNode {...props} />);
70
- const node = screen.getByTestId("graph-task-node-FN-TEST");
71
- expect(node.className).toContain("graph-task-node--active");
72
- expect(container.querySelector(".card")?.className).toContain("agent-active");
73
- expect(screen.getByText("Executing")).toBeTruthy();
74
- expect(container.querySelector(".graph-task-active-indicator")).toBeTruthy();
75
- });
76
-
77
- it("defaults indicator text to Executing when status is missing on in-progress tasks", () => {
78
- const props = createProps(createTask({ column: "in-progress", status: undefined }));
79
- const { container } = render(<GraphTaskNode {...props} />);
80
-
81
- expect(container.querySelector(".graph-task-active-indicator")).toBeTruthy();
82
- expect(screen.getByText("Executing")).toBeTruthy();
83
- });
84
-
85
- it("applies in-review visual class and does not apply active class for in-review tasks", () => {
86
- const props = createProps(createTask({ column: "in-review", status: "idle" }));
87
- const { container } = render(<GraphTaskNode {...props} />);
88
-
89
- const node = screen.getByTestId("graph-task-node-FN-TEST");
90
- expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
91
- expect(node.className).toContain("graph-task-node--in-review");
92
- expect(node.className).not.toContain("graph-task-node--active");
93
- });
94
-
95
- it.each(["todo", "triage", "in-progress"] as const)("does not apply in-review class for %s tasks", (column) => {
96
- const props = createProps(createTask({ column, status: column === "in-progress" ? "executing" : "idle" }));
97
-
98
- render(<GraphTaskNode {...props} />);
99
- expect(screen.getByTestId("graph-task-node-FN-TEST").className).not.toContain("graph-task-node--in-review");
100
- });
101
-
102
- it("does not render active indicator for paused in-progress tasks", () => {
103
- const props = createProps(createTask({ column: "in-progress", status: "executing", paused: true }));
104
- const { container } = render(<GraphTaskNode {...props} />);
105
-
106
- expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
107
- });
108
-
109
- it("does not render active indicator for failed in-progress tasks", () => {
110
- const props = createProps(createTask({ column: "in-progress", status: "failed" }));
111
- const { container } = render(<GraphTaskNode {...props} />);
112
-
113
- expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
114
- });
115
-
116
- it("sets current-step attribute for active task when current step is valid", () => {
117
- const props = createProps(
118
- createTask({
119
- column: "in-progress",
120
- status: "executing",
121
- steps: [
122
- { name: "step one", status: "done" },
123
- { name: "step two", status: "done" },
124
- { name: "step three", status: "in-progress" },
125
- ],
126
- currentStep: 2,
127
- }),
128
- );
129
-
130
- render(<GraphTaskNode {...props} />);
131
- expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("2");
132
- });
133
-
134
- it("sets current-step attribute to zero when first step is active", () => {
135
- const props = createProps(
136
- createTask({
137
- column: "in-progress",
138
- status: "executing",
139
- steps: [{ name: "step one", status: "in-progress" }],
140
- currentStep: 0,
141
- }),
142
- );
143
-
144
- render(<GraphTaskNode {...props} />);
145
- expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("0");
146
- });
147
-
148
- it("omits current-step attribute when current step is out of bounds", () => {
149
- const props = createProps(
150
- createTask({
151
- column: "in-progress",
152
- status: "executing",
153
- steps: [{ name: "step one", status: "in-progress" }],
154
- currentStep: 10,
155
- }),
156
- );
157
-
158
- render(<GraphTaskNode {...props} />);
159
- expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
160
- });
161
-
162
- it("omits current-step attribute when current step is negative", () => {
163
- const props = createProps(createTask({ column: "in-progress", status: "executing", steps: [], currentStep: -1 }));
164
-
165
- render(<GraphTaskNode {...props} />);
166
- expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
167
- });
168
-
169
- it("omits current-step attribute when current step is undefined", () => {
170
- const props = createProps(createTask({ column: "in-progress", status: "executing", steps: [], currentStep: undefined }));
171
-
172
- render(<GraphTaskNode {...props} />);
173
- expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
174
- });
175
-
176
- it("does not set current-step for non-active tasks", () => {
177
- const props = createProps(
178
- createTask({
179
- column: "todo",
180
- status: "queued",
181
- steps: [{ name: "step one", status: "in-progress" }],
182
- currentStep: 0,
183
- }),
184
- );
185
-
186
- render(<GraphTaskNode {...props} />);
187
- expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
188
- });
189
-
190
- it("sets current-step to native step index when workflow steps are present", () => {
191
- const props = createProps(
192
- createTask({
193
- column: "in-progress",
194
- status: "executing",
195
- steps: [
196
- { id: "native-1", name: "native one", status: "done" },
197
- { id: "native-2", name: "native two", status: "in-progress" },
198
- ],
199
- enabledWorkflowSteps: ["wf-1"],
200
- currentStep: 1,
201
- }),
202
- );
203
-
204
- render(<GraphTaskNode {...props} />);
205
- expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("1");
206
- });
207
-
208
- it("does not set current-step when native step list is empty even with workflow steps", () => {
209
- const props = createProps(
210
- createTask({
211
- column: "in-progress",
212
- status: "executing",
213
- steps: [],
214
- enabledWorkflowSteps: ["wf-1"],
215
- currentStep: 0,
216
- }),
217
- );
218
-
219
- render(<GraphTaskNode {...props} />);
220
- expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
221
- });
222
-
223
- it("clicking card opens task detail exactly once", () => {
224
- const props = createProps(createTask());
225
- const { container } = render(<GraphTaskNode {...props} />);
226
-
227
- const card = container.querySelector(".card");
228
- expect(card).toBeTruthy();
229
- fireEvent.click(card!);
230
- expect(props.onOpenDetail).toHaveBeenCalledTimes(1);
231
- expect(props.onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-TEST" }));
232
- });
233
-
234
- it("clicking active indicator surface opens task detail", () => {
235
- const props = createProps(createTask({ column: "in-progress", status: "executing" }));
236
- const { container } = render(<GraphTaskNode {...props} />);
237
-
238
- const indicator = container.querySelector(".graph-task-active-indicator");
239
- expect(indicator).toBeTruthy();
240
- fireEvent.click(indicator!);
241
-
242
- expect(props.onOpenDetail).toHaveBeenCalledTimes(1);
243
- expect(props.onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-TEST" }));
244
- });
245
-
246
- it("applies highlighted class only when requested", () => {
247
- const highlightedProps = createProps(createTask({ id: "FN-HL" }));
248
- const neutralProps = createProps(createTask({ id: "FN-NEUTRAL" }));
249
-
250
- const { unmount } = render(<GraphTaskNode {...highlightedProps} isHighlighted={true} />);
251
- expect(screen.getByTestId("graph-task-node-FN-HL").className).toContain("graph-task-node--highlighted");
252
- unmount();
253
-
254
- render(<GraphTaskNode {...neutralProps} />);
255
- const neutral = screen.getByTestId("graph-task-node-FN-NEUTRAL");
256
- expect(neutral.className).not.toContain("graph-task-node--highlighted");
257
- expect(neutral.className).not.toContain("graph-task-node--dimmed");
258
- });
259
-
260
- it("renders the same TaskCard structure as board usage", () => {
261
- const task = createTask({
262
- id: "FN-SAME",
263
- column: "in-progress",
264
- status: "executing",
265
- error: "Execution failed",
266
- missionId: "M-1",
267
- sourceType: "automation",
268
- sourceAgentId: "agent-1",
269
- steps: [{ name: "sync", status: "in-progress" }],
270
- currentStep: 0,
271
- });
272
- const props = createProps(task);
273
-
274
- const { container } = render(
275
- <div>
276
- <TaskCard {...props} disableDrag={true} />
277
- <GraphTaskNode {...props} />
278
- </div>,
279
- );
280
-
281
- const cards = container.querySelectorAll(".card");
282
- expect(cards.length).toBe(2);
283
-
284
- const [boardCard, graphCard] = cards;
285
- const selectors = [
286
- ".card-id",
287
- ".card-title",
288
- ".card-status-badge",
289
- ".card-step-dot",
290
- ".card-step-name",
291
- ".card-progress",
292
- ".card-progress-fill",
293
- ".card-error",
294
- ".card-mission-badge",
295
- ".card-provider-icons",
296
- ".card-agent-badge",
297
- ];
298
-
299
- for (const selector of selectors) {
300
- expect(Boolean(boardCard.querySelector(selector))).toBe(Boolean(graphCard.querySelector(selector)));
301
- }
302
-
303
- expect(Boolean(boardCard.querySelector(".card-error"))).toBe(Boolean(graphCard.querySelector(".card-error")));
304
- expect(boardCard.querySelector(".card-id")?.textContent).toBe(graphCard.querySelector(".card-id")?.textContent);
305
- expect(boardCard.querySelector(".card-title")?.textContent).toBe(graphCard.querySelector(".card-title")?.textContent);
306
- });
307
- });
@@ -1,60 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
- import { GraphToolbar } from "../GraphToolbar";
4
-
5
- describe("GraphToolbar", () => {
6
- afterEach(() => {
7
- cleanup();
8
- });
9
- it("renders controls and zoom percent", () => {
10
- render(
11
- <GraphToolbar
12
- zoom={1.25}
13
- onZoomIn={vi.fn()}
14
- onZoomOut={vi.fn()}
15
- onFitToGraph={vi.fn()}
16
- onResetView={vi.fn()}
17
- />,
18
- );
19
-
20
- expect(screen.getByRole("button", { name: "Zoom in" })).toBeTruthy();
21
- expect(screen.getByRole("button", { name: "Zoom out" })).toBeTruthy();
22
- expect(screen.getByRole("button", { name: "Fit to graph" })).toBeTruthy();
23
- expect(screen.getByRole("button", { name: "Reset view" })).toBeTruthy();
24
- expect(screen.getByText("125%")).toBeTruthy();
25
- });
26
-
27
- it("fires callbacks", () => {
28
- const onZoomIn = vi.fn();
29
- const onZoomOut = vi.fn();
30
- const onFitToGraph = vi.fn();
31
- const onResetView = vi.fn();
32
-
33
- render(
34
- <GraphToolbar
35
- zoom={1}
36
- onZoomIn={onZoomIn}
37
- onZoomOut={onZoomOut}
38
- onFitToGraph={onFitToGraph}
39
- onResetView={onResetView}
40
- />,
41
- );
42
-
43
- fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
44
- fireEvent.click(screen.getByRole("button", { name: "Zoom out" }));
45
- fireEvent.click(screen.getByRole("button", { name: "Fit to graph" }));
46
- fireEvent.click(screen.getByRole("button", { name: "Reset view" }));
47
-
48
- expect(onZoomIn).toHaveBeenCalledOnce();
49
- expect(onZoomOut).toHaveBeenCalledOnce();
50
- expect(onFitToGraph).toHaveBeenCalledOnce();
51
- expect(onResetView).toHaveBeenCalledOnce();
52
- });
53
-
54
- it("applies toolbar class", () => {
55
- render(
56
- <GraphToolbar zoom={1} onZoomIn={vi.fn()} onZoomOut={vi.fn()} onFitToGraph={vi.fn()} onResetView={vi.fn()} />,
57
- );
58
- expect(screen.getByTestId("graph-toolbar").className).toContain("graph-toolbar");
59
- });
60
- });
@@ -1,75 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest";
2
- import { cleanup, render, screen } from "@testing-library/react";
3
- import { GraphEdges } from "../edges";
4
- import type { GraphEdge } from "../types";
5
-
6
- function renderEdges(edges: GraphEdge[], highlightedEdgeIds?: Set<string>) {
7
- const positions = new Map([
8
- ["A", { x: 0, y: 0 }],
9
- ["B", { x: 320, y: 180 }],
10
- ["C", { x: 640, y: 180 }],
11
- ]);
12
-
13
- return render(
14
- <GraphEdges
15
- edges={edges}
16
- positions={positions}
17
- highlightedEdgeIds={highlightedEdgeIds}
18
- />,
19
- );
20
- }
21
-
22
- describe("GraphEdges", () => {
23
- afterEach(() => {
24
- cleanup();
25
- });
26
- it("renders single edge", () => {
27
- renderEdges([{ source: "A", target: "B" }]);
28
- const edge = screen.getAllByTestId("dependency-edge")[0];
29
- expect(edge.getAttribute("opacity")).toBe("1");
30
- expect(edge.getAttribute("stroke")).toBe("var(--border)");
31
- });
32
-
33
- it("renders multiple edges", () => {
34
- renderEdges([
35
- { source: "A", target: "B" },
36
- { source: "A", target: "C" },
37
- ]);
38
- expect(screen.getAllByTestId("dependency-edge")).toHaveLength(2);
39
- });
40
-
41
- it("supports edges with same source", () => {
42
- renderEdges([
43
- { source: "A", target: "B" },
44
- { source: "A", target: "C" },
45
- ]);
46
- expect(screen.getAllByTestId("dependency-edge")).toHaveLength(2);
47
- });
48
-
49
- it("supports edges with same target", () => {
50
- renderEdges([
51
- { source: "B", target: "A" },
52
- { source: "C", target: "A" },
53
- ]);
54
- expect(screen.getAllByTestId("dependency-edge")).toHaveLength(2);
55
- });
56
-
57
- it("dims non-highlighted edges when highlight set provided", () => {
58
- renderEdges(
59
- [
60
- { source: "A", target: "B" },
61
- { source: "A", target: "C" },
62
- ],
63
- new Set(["A->B"]),
64
- );
65
-
66
- const all = screen.getAllByTestId("dependency-edge");
67
- const highlighted = all.find((edge) => edge.getAttribute("data-edge-id") === "A->B");
68
- const dimmed = all.find((edge) => edge.getAttribute("data-edge-id") === "A->C");
69
-
70
- expect(highlighted?.getAttribute("opacity")).toBe("1");
71
- expect(highlighted?.getAttribute("class") ?? "").toContain("graph-edge--highlighted");
72
- expect(dimmed?.getAttribute("opacity")).toBe("0.15");
73
- expect(dimmed?.getAttribute("class") ?? "").toContain("graph-edge--dimmed");
74
- });
75
- });
@@ -1,62 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { render, renderHook } from "@testing-library/react";
3
- import type { Task } from "@fusion/core";
4
- import { filterGraphTasks } from "../filters";
5
- import { useGraphData } from "../useGraphData";
6
- import { DependencyGraph } from "../DependencyGraph";
7
-
8
- function createTask(id: string, column: Task["column"], dependencies: string[] = [], status?: Task["status"]): Task {
9
- return {
10
- id,
11
- description: id,
12
- column,
13
- status,
14
- dependencies,
15
- steps: [],
16
- currentStep: 0,
17
- log: [],
18
- } as Task;
19
- }
20
-
21
- describe("dependency graph filtering", () => {
22
- it("includes triage/todo/in-progress/in-review and excludes done/archived by column", () => {
23
- const tasks = [
24
- createTask("T", "triage", [], "done"),
25
- createTask("TD", "todo", [], "done"),
26
- createTask("P", "in-progress", [], "done"),
27
- createTask("R", "in-review", [], "done"),
28
- createTask("D", "done", [], "in-progress"),
29
- createTask("A", "archived", [], "in-progress"),
30
- ];
31
-
32
- expect(filterGraphTasks(tasks).map((task) => task.id)).toEqual(["T", "TD", "P", "R"]);
33
- });
34
-
35
- it("keeps standalone tasks without dependencies as nodes", () => {
36
- const tasks = [createTask("A", "todo")];
37
- const { result } = renderHook(() => useGraphData(tasks));
38
-
39
- expect(result.current.nodes.map((node) => node.task.id)).toEqual(["A"]);
40
- expect(result.current.edges).toEqual([]);
41
- });
42
-
43
- it("keeps only edges to included dependency tasks for mixed-status dependencies", () => {
44
- const filteredTasks = filterGraphTasks([
45
- createTask("A", "in-progress", ["B", "DONE", "ARCH"]),
46
- createTask("B", "todo"),
47
- createTask("DONE", "done"),
48
- createTask("ARCH", "archived"),
49
- ]);
50
-
51
- const { result } = renderHook(() => useGraphData(filteredTasks));
52
- expect(result.current.edges).toEqual([{ source: "A", target: "B" }]);
53
- });
54
-
55
- it("renders empty state when all tasks are done/archived", () => {
56
- const { container } = render(
57
- <DependencyGraph tasks={[createTask("D", "done"), createTask("A", "archived")]} onOpenTaskDetail={() => {}} />,
58
- );
59
-
60
- expect(container.textContent).toContain("No active tasks");
61
- });
62
- });
@@ -1,78 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { Task } from "@fusion/core";
3
- import { EXCLUDED_COLUMNS, filterGraphTasks, INCLUDED_COLUMNS } from "../filters";
4
-
5
- function createTask(id: string, column: Task["column"], dependencies: string[] = []): Task {
6
- return {
7
- id,
8
- description: `Task ${id}`,
9
- column,
10
- dependencies,
11
- steps: [],
12
- currentStep: 0,
13
- log: [],
14
- } as Task;
15
- }
16
-
17
- describe("filterGraphTasks", () => {
18
- it("returns empty for empty input", () => {
19
- expect(filterGraphTasks([])).toEqual([]);
20
- });
21
-
22
- it("includes tasks from all included columns", () => {
23
- const tasks = Array.from(INCLUDED_COLUMNS).map((column, index) => createTask(`FN-${index + 1}`, column));
24
-
25
- expect(filterGraphTasks(tasks)).toEqual(tasks);
26
- });
27
-
28
- it("returns empty when only excluded columns are present", () => {
29
- const tasks = Array.from(EXCLUDED_COLUMNS).map((column, index) => createTask(`FN-${index + 1}`, column));
30
-
31
- expect(filterGraphTasks(tasks)).toEqual([]);
32
- });
33
-
34
- it("includes and excludes exact columns for mixed input", () => {
35
- const tasks = [
36
- createTask("FN-1", "triage"),
37
- createTask("FN-2", "todo"),
38
- createTask("FN-3", "in-progress"),
39
- createTask("FN-4", "in-review"),
40
- createTask("FN-5", "done"),
41
- createTask("FN-6", "archived"),
42
- ];
43
-
44
- expect(filterGraphTasks(tasks).map((task) => task.id)).toEqual(["FN-1", "FN-2", "FN-3", "FN-4"]);
45
- });
46
-
47
- it.each([
48
- ["triage", true],
49
- ["todo", true],
50
- ["in-progress", true],
51
- ["in-review", true],
52
- ["done", false],
53
- ["archived", false],
54
- ] as const)("column %s inclusion=%s", (column, included) => {
55
- const task = createTask("FN-1", column);
56
- const result = filterGraphTasks([task]);
57
-
58
- expect(result.length > 0).toBe(included);
59
- });
60
-
61
- it("gracefully excludes tasks with invalid columns", () => {
62
- const invalidTask = {
63
- ...createTask("FN-invalid", "todo"),
64
- column: undefined,
65
- } as unknown as Task;
66
-
67
- expect(filterGraphTasks([invalidTask])).toEqual([]);
68
- });
69
-
70
- it("preserves task object identity", () => {
71
- const taskA = createTask("FN-1", "todo");
72
- const taskB = createTask("FN-2", "in-review");
73
- const result = filterGraphTasks([taskA, taskB]);
74
-
75
- expect(result[0]).toBe(taskA);
76
- expect(result[1]).toBe(taskB);
77
- });
78
- });
@@ -1,95 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { clearPositions, loadPositions, mergePositions, savePositions } from "../utils/graphPositionStorage";
3
-
4
- function createStorage() {
5
- const store = new Map<string, string>();
6
- return {
7
- getItem: (key: string) => store.get(key) ?? null,
8
- setItem: (key: string, value: string) => {
9
- store.set(key, value);
10
- },
11
- removeItem: (key: string) => {
12
- store.delete(key);
13
- },
14
- };
15
- }
16
-
17
- describe("graphPositionStorage", () => {
18
- beforeEach(() => {
19
- vi.unstubAllGlobals();
20
- vi.stubGlobal("window", { localStorage: createStorage() });
21
- });
22
-
23
- it("loadPositions returns parsed positions from localStorage", () => {
24
- window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ a: { x: 1, y: 2 } }));
25
- expect(loadPositions("p1")).toEqual({ a: { x: 1, y: 2 } });
26
- });
27
-
28
- it("loadPositions returns empty object when localStorage is empty", () => {
29
- expect(loadPositions("p1")).toEqual({});
30
- });
31
-
32
- it("loadPositions returns empty object for invalid json", () => {
33
- window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", "{oops");
34
- expect(loadPositions("p1")).toEqual({});
35
- });
36
-
37
- it("loadPositions skips entries with invalid position shape", () => {
38
- window.localStorage.setItem(
39
- "kb:p1:fusion-plugin-dependency-graph:positions",
40
- JSON.stringify({
41
- good: { x: 1, y: 2 },
42
- badX: { x: "1", y: 2 },
43
- badY: { x: 1, y: null },
44
- }),
45
- );
46
-
47
- expect(loadPositions("p1")).toEqual({ good: { x: 1, y: 2 } });
48
- });
49
-
50
- it("savePositions writes filtered positions json to scoped localStorage key", () => {
51
- savePositions({ a: { x: 1, y: 2 }, b: { x: 3, y: 4 } }, new Set(["a"]), "p1");
52
- expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBe(JSON.stringify({ a: { x: 1, y: 2 } }));
53
- });
54
-
55
- it("clearPositions removes scoped localStorage key", () => {
56
- window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ a: { x: 1, y: 2 } }));
57
- clearPositions("p1");
58
- expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBeNull();
59
- });
60
-
61
- it("mergePositions prefers saved for overlap and keeps auto-layout for new tasks", () => {
62
- expect(
63
- mergePositions(
64
- { a: { x: 1, y: 1 }, b: { x: 2, y: 2 } },
65
- { a: { x: 10, y: 10 } },
66
- new Set(["a", "b"]),
67
- ),
68
- ).toEqual({ a: { x: 10, y: 10 }, b: { x: 2, y: 2 } });
69
- });
70
-
71
- it("mergePositions omits non-visible ids", () => {
72
- expect(
73
- mergePositions(
74
- { a: { x: 1, y: 1 }, hidden: { x: 9, y: 9 } },
75
- { hidden: { x: 10, y: 10 } },
76
- new Set(["a"]),
77
- ),
78
- ).toEqual({ a: { x: 1, y: 1 } });
79
- });
80
-
81
- it("mergePositions returns auto-layout unchanged when saved is empty", () => {
82
- expect(mergePositions({ a: { x: 1, y: 2 } }, {}, new Set(["a"]))).toEqual({ a: { x: 1, y: 2 } });
83
- });
84
-
85
- it("loadPositions returns empty object when localStorage.getItem is unavailable", () => {
86
- vi.stubGlobal("window", { localStorage: {} });
87
- expect(loadPositions("p1")).toEqual({});
88
- });
89
-
90
- it("savePositions and clearPositions are no-ops when localStorage methods are unavailable", () => {
91
- vi.stubGlobal("window", { localStorage: {} });
92
- expect(() => savePositions({ a: { x: 1, y: 2 } }, new Set(["a"]), "p1")).not.toThrow();
93
- expect(() => clearPositions("p1")).not.toThrow();
94
- });
95
- });