@runfusion/fusion 0.22.0 → 0.24.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.
- package/dist/bin.js +30071 -20735
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
- package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
- package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
- package/dist/client/assets/AgentsView-CV3vm7Qk.css +1 -0
- package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
- package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
- package/dist/client/assets/{DevServerView-l8RCyL2k.js → DevServerView-BkvtjZBa.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-CS1dwqcC.js → DirectoryPicker-BK-KbnhP.js} +1 -1
- package/dist/client/assets/{DocumentsView-DmthQWDZ.js → DocumentsView-BEg1CQAk.js} +1 -1
- package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
- package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
- package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
- package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
- package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
- package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
- package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
- package/dist/client/assets/{MemoryView-CPwlKnUI.js → MemoryView-CKElJY_3.js} +2 -2
- package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
- package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
- package/dist/client/assets/{PiExtensionsManager-j8rPXqmB.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
- package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
- package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
- package/dist/client/assets/{ResearchView-D9DNJYDq.js → ResearchView-B256Lr8I.js} +1 -1
- package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
- package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
- package/dist/client/assets/{SettingsModal-fxvTFLtR.js → SettingsModal-yRqM4DV8.js} +1 -1
- package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
- package/dist/client/assets/{SkillsView-Ddf0YL8z.js → SkillsView-CP8JX0P_.js} +1 -1
- package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
- package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
- package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
- package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
- package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
- package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
- package/dist/client/assets/{folder-open-BiJpmnaT.js → folder-open-DHjELt8-.js} +1 -1
- package/dist/client/assets/index-CQyVRLOb.js +692 -0
- package/dist/client/assets/index-CxA2Nn0_.css +1 -0
- package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
- package/dist/client/assets/{star-BwRZmiuZ.js → star-DYesq1AV.js} +1 -1
- package/dist/client/assets/{upload-D4NwZhPp.js → upload-DTWF3Db5.js} +1 -1
- package/dist/client/assets/{users-DNISDtI1.js → users--syrel4l.js} +1 -1
- package/dist/client/index.html +12 -20
- package/dist/client/theme-data.css +106 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/extension.js +17072 -9627
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
- package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
- package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
- package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
- package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +176 -7
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
- package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
- package/dist/plugins/fusion-plugin-reports/package.json +26 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
- package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
- package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
- package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
- package/package.json +2 -2
- package/skill/fusion/SKILL.md +2 -2
- package/skill/fusion/references/engine-tools.md +8 -2
- package/skill/fusion/references/extension-tools.md +39 -0
- package/skill/fusion/references/fusion-capabilities.md +3 -0
- package/dist/client/assets/AgentDetailView-BKKpbp1S.js +0 -18
- package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
- package/dist/client/assets/AgentsView-BRXFmrcJ.js +0 -527
- package/dist/client/assets/AgentsView-Bs03ptrd.css +0 -1
- package/dist/client/assets/ChatView-D7L2e_qu.js +0 -1
- package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
- package/dist/client/assets/InsightsView-DvXpMKmH.js +0 -11
- package/dist/client/assets/NodesView-BLlfUfsy.js +0 -14
- package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
- package/dist/client/assets/PluginManager-DA_T0GHn.css +0 -1
- package/dist/client/assets/PluginManager-pW6RMz5z.js +0 -1
- package/dist/client/assets/RoadmapsView-Djc_X35v.js +0 -6
- package/dist/client/assets/SettingsModal-BWe0KrGY.css +0 -1
- package/dist/client/assets/SettingsModal-WGCF_pk8.js +0 -31
- package/dist/client/assets/SetupWizardModal-tG_MF_nA.js +0 -1
- package/dist/client/assets/agentSkills-EwIwBlG8.js +0 -1
- package/dist/client/assets/index-D6ebxTPF.css +0 -1
- package/dist/client/assets/index-DYDLmOcK.js +0 -694
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -132
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -31
- package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -23
- /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
});
|
package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
});
|