Skip to content

Commit a6321fb

Browse files
authored
Merge pull request #220 from backbay-labs/feat/swarm-engine
feat: @clawdstrike/swarm-engine — browser-safe swarm orchestration with guard pipeline
2 parents 11f71e1 + 1cd7103 commit a6321fb

File tree

77 files changed

+26131
-350
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+26131
-350
lines changed

apps/workbench/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/workbench/package.json

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
"version": "0.1.0",
55
"type": "module",
66
"scripts": {
7+
"build:swarm-engine": "npm --prefix ../../packages/swarm-engine run build",
8+
"predev": "npm run build:swarm-engine",
9+
"prebuild": "npm run build:swarm-engine",
10+
"postbuild": "vitest run src/__tests__/observatory-bundle-graph-smoke.test.ts",
11+
"prepreview": "npm run build:swarm-engine",
12+
"pretauri:dev": "npm run build:swarm-engine",
13+
"pretauri:build": "npm run build:swarm-engine",
14+
"pretest": "npm run build:swarm-engine",
15+
"pretest:e2e": "npm run build:swarm-engine",
16+
"pretest:e2e:headed": "npm run build:swarm-engine",
17+
"pretest:e2e:ui": "npm run build:swarm-engine",
18+
"pretypecheck": "npm run build:swarm-engine",
719
"dev": "vite",
820
"build": "vite build",
921
"tauri:prepare": "bun run scripts/prepare-tauri-build.ts",
@@ -14,9 +26,11 @@
1426
"test": "vitest --passWithNoTests",
1527
"typecheck": "NODE_OPTIONS=--max-old-space-size=8192 npm exec tsc -- --noEmit",
1628
"lint": "eslint src/",
17-
"test:e2e": "playwright test",
1829
"dogfood:live": "bash ../../scripts/workbench-live-dogfood.sh",
19-
"dogfood:missions": "bash ../../scripts/workbench-mission-control-dogfood.sh"
30+
"dogfood:missions": "bash ../../scripts/workbench-mission-control-dogfood.sh",
31+
"test:e2e": "playwright test",
32+
"test:e2e:headed": "playwright test --headed",
33+
"test:e2e:ui": "playwright test --ui"
2034
},
2135
"dependencies": {
2236
"@base-ui/react": "^1.2.0",
@@ -65,7 +79,8 @@
6579
"three": "^0.183.2",
6680
"wawa-vfx": "^1.2.10",
6781
"yaml": "^2.7.0",
68-
"zustand": "^5.0.12"
82+
"zustand": "^5.0.12",
83+
"@clawdstrike/swarm-engine": "file:../../packages/swarm-engine"
6984
},
7085
"devDependencies": {
7186
"@modelcontextprotocol/sdk": "^1.12.1",

apps/workbench/playwright.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { defineConfig, devices } from "@playwright/test";
22

33
export default defineConfig({
4-
testDir: "./e2e",
4+
testDir: ".",
5+
testMatch: ["e2e/**/*.spec.ts", "tests/e2e/**/*.spec.ts"],
56
timeout: 60_000,
7+
expect: { timeout: 10_000 },
68
retries: process.env.CI ? 2 : 0,
9+
workers: process.env.CI ? 1 : undefined,
10+
reporter: process.env.CI ? "github" : "list",
711
use: {
812
baseURL: "http://127.0.0.1:1421",
913
trace: "on-first-retry",
14+
screenshot: "only-on-failure",
15+
video: "retain-on-failure",
1016
},
1117
webServer: {
1218
command: "npm run dev -- --host 127.0.0.1 --port 1421",

apps/workbench/src/__tests__/observatory-bundle-graph-smoke.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ import { join } from "node:path";
33
import { describe, expect, it } from "vitest";
44

55
const distAssetsDir = join(process.cwd(), "dist/assets");
6-
const runSmoke = existsSync(distAssetsDir);
6+
const requiredAssetPrefixes = [
7+
"ObservatoryWorldCanvas-",
8+
"ObservatoryFlowRuntimeScene-",
9+
] as const;
10+
const builtAssets = existsSync(distAssetsDir) ? readdirSync(distAssetsDir) : [];
11+
const runSmoke = requiredAssetPrefixes.every((prefix) =>
12+
builtAssets.some((entry) => entry.startsWith(prefix)),
13+
);
714
const describeIfBuild = runSmoke ? describe : describe.skip;
815

916
function findAssetByPrefix(prefix: string) {
10-
const assetName = readdirSync(distAssetsDir).find((entry) => entry.startsWith(prefix));
17+
const assetName = builtAssets.find((entry) => entry.startsWith(prefix));
1118
if (!assetName) {
1219
throw new Error(`Could not find a built asset with prefix "${prefix}" in ${distAssetsDir}`);
1320
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React from "react";
2+
import { act, render, screen } from "@testing-library/react";
3+
import { beforeEach, describe, expect, it, vi } from "vitest";
4+
import { MemoryRouter } from "react-router-dom";
5+
import { SwarmBoardPage } from "../swarm-board-page";
6+
7+
vi.mock("@xyflow/react", () => {
8+
const ReactFlowMock = ({ children }: { children?: React.ReactNode }) => (
9+
<div data-testid="react-flow-canvas">{children}</div>
10+
);
11+
12+
return {
13+
ReactFlow: ReactFlowMock,
14+
ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
15+
MiniMap: () => <div data-testid="react-flow-minimap" />,
16+
useReactFlow: () => ({
17+
getViewport: () => ({ x: 0, y: 0, zoom: 1 }),
18+
fitView: vi.fn(),
19+
setViewport: vi.fn(),
20+
zoomIn: vi.fn(),
21+
zoomOut: vi.fn(),
22+
}),
23+
applyNodeChanges: (_changes: unknown[], nodes: unknown[]) => nodes,
24+
applyEdgeChanges: (_changes: unknown[], edges: unknown[]) => edges,
25+
MarkerType: { ArrowClosed: "arrowclosed" },
26+
};
27+
});
28+
29+
vi.mock("@/features/swarm/hooks/use-coordinator-board-bridge", () => ({
30+
useCoordinatorBoardBridge: vi.fn(),
31+
}));
32+
33+
vi.mock("@/features/swarm/hooks/use-policy-eval-board-bridge", () => ({
34+
usePolicyEvalBoardBridge: vi.fn(),
35+
}));
36+
37+
vi.mock("@/features/swarm/hooks/use-trust-graph-bridge", () => ({
38+
useTrustGraphBridge: vi.fn(),
39+
}));
40+
41+
vi.mock("@/features/swarm/hooks/use-receipt-flow-bridge", () => ({
42+
useReceiptFlowBridge: vi.fn(),
43+
receiptEdgeTimestamps: new Map(),
44+
}));
45+
46+
vi.mock("@/features/swarm/hooks/use-engine-board-bridge", () => ({
47+
useEngineBoardBridge: vi.fn(),
48+
}));
49+
50+
vi.mock("@/features/swarm/coordinator-instance", () => ({
51+
getCoordinator: () => ({
52+
isConnected: false,
53+
outboxSize: 0,
54+
joinedSwarmIds: [],
55+
}),
56+
}));
57+
58+
vi.mock("@/features/swarm/stores/swarm-engine-provider", () => ({
59+
SwarmEngineProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
60+
useOptionalSwarmEngine: () => null,
61+
}));
62+
63+
vi.mock("../swarm-board-toolbar", () => ({
64+
SwarmBoardToolbar: () => <div data-testid="swarm-board-toolbar" />,
65+
}));
66+
67+
vi.mock("../swarm-board-left-rail", () => ({
68+
SwarmBoardLeftRail: () => <div data-testid="swarm-board-left-rail" />,
69+
}));
70+
71+
vi.mock("../swarm-board-inspector", () => ({
72+
SwarmBoardInspector: () => <div data-testid="swarm-board-inspector" />,
73+
}));
74+
75+
vi.mock("../nodes", () => ({
76+
swarmBoardNodeTypes: {},
77+
}));
78+
79+
vi.mock("../edges", () => ({
80+
swarmBoardEdgeTypes: {},
81+
}));
82+
83+
vi.mock("@/lib/workbench/use-terminal-sessions", () => ({
84+
useTerminalSessionsFromBoard: () => ({
85+
spawnSession: vi.fn(),
86+
killSession: vi.fn(),
87+
}),
88+
}));
89+
90+
vi.mock("@/lib/workbench/terminal-service", () => ({
91+
terminalService: {
92+
getCwd: vi.fn().mockResolvedValue("/mock/cwd"),
93+
create: vi.fn().mockResolvedValue({ id: "mock-session", branch: "main" }),
94+
write: vi.fn().mockResolvedValue(undefined),
95+
kill: vi.fn().mockResolvedValue(undefined),
96+
onExit: vi.fn().mockResolvedValue(() => {}),
97+
},
98+
worktreeService: {
99+
create: vi.fn().mockResolvedValue({ path: "/mock/worktree", branch: "test-branch" }),
100+
remove: vi.fn().mockResolvedValue(undefined),
101+
},
102+
}));
103+
104+
describe("SwarmBoardPage provider safety", () => {
105+
beforeEach(() => {
106+
localStorage.clear();
107+
});
108+
109+
it("renders even when the engine context is unavailable", async () => {
110+
await act(async () => {
111+
render(
112+
<MemoryRouter initialEntries={["/workbench/swarm-board"]}>
113+
<SwarmBoardPage />
114+
</MemoryRouter>,
115+
);
116+
await Promise.resolve();
117+
});
118+
119+
expect(screen.getByTestId("swarm-board-toolbar")).toBeInTheDocument();
120+
expect(screen.getByTestId("react-flow-canvas")).toBeInTheDocument();
121+
});
122+
});

apps/workbench/src/components/workbench/swarm-board/edges/swarm-edge.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const ACTIVITY_RECENCY_MS = 3000;
4848
// Edge type visual config — restrained, functional colors
4949
// ---------------------------------------------------------------------------
5050

51-
type SwarmEdgeType = "handoff" | "spawned" | "artifact" | "receipt";
51+
type SwarmEdgeType = "handoff" | "spawned" | "artifact" | "receipt" | "topology";
5252

5353
interface EdgeStyleConfig {
5454
color: string;
@@ -86,6 +86,13 @@ const EDGE_STYLES: Record<SwarmEdgeType, EdgeStyleConfig> = {
8686
animated: true,
8787
dotSize: 5,
8888
},
89+
topology: {
90+
color: "#3d4250",
91+
strokeWidth: 0.75,
92+
strokeDasharray: "3 6",
93+
animated: false,
94+
dotSize: 4,
95+
},
8996
};
9097

9198
const DEFAULT_STYLE: EdgeStyleConfig = {
@@ -118,7 +125,7 @@ export function SwarmEdge({
118125

119126
// Resolve edge type from data or fall back to label heuristic
120127
const edgeType = (data?.edgeType as SwarmEdgeType) ?? undefined;
121-
const config = edgeType ? EDGE_STYLES[edgeType] : DEFAULT_STYLE;
128+
const config = (edgeType && EDGE_STYLES[edgeType]) ?? DEFAULT_STYLE;
122129

123130
// Hover-reveal: check if this edge connects to the hovered or selected node
124131
const hoveredNodeId = data?.hoveredNodeId as string | null | undefined;

0 commit comments

Comments
 (0)