claude-code-swarm 0.3.3 → 0.3.5

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 (273) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +22 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +76 -0
  4. package/.claude-plugin/run-minimem-mcp.sh +98 -0
  5. package/.claude-plugin/run-opentasks-mcp.sh +65 -0
  6. package/CLAUDE.md +200 -36
  7. package/README.md +65 -0
  8. package/e2e/helpers/cleanup.mjs +17 -3
  9. package/e2e/helpers/map-mock-server.mjs +201 -25
  10. package/e2e/helpers/sidecar.mjs +222 -0
  11. package/e2e/helpers/workspace.mjs +2 -1
  12. package/e2e/tier5-sidecar-inbox.test.mjs +900 -0
  13. package/e2e/tier6-inbox-mcp.test.mjs +173 -0
  14. package/e2e/tier6-live-agent.test.mjs +759 -0
  15. package/e2e/vitest.config.e2e.mjs +1 -1
  16. package/hooks/hooks.json +15 -8
  17. package/package.json +13 -1
  18. package/references/agent-inbox/CLAUDE.md +151 -0
  19. package/references/agent-inbox/README.md +238 -0
  20. package/references/agent-inbox/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  21. package/references/agent-inbox/docs/DESIGN.md +1156 -0
  22. package/references/agent-inbox/hooks/inbox-hook.mjs +119 -0
  23. package/references/agent-inbox/hooks/register-hook.mjs +69 -0
  24. package/references/agent-inbox/package-lock.json +3347 -0
  25. package/references/agent-inbox/package.json +58 -0
  26. package/references/agent-inbox/rules/agent-inbox.md +78 -0
  27. package/references/agent-inbox/src/federation/address.ts +61 -0
  28. package/references/agent-inbox/src/federation/connection-manager.ts +573 -0
  29. package/references/agent-inbox/src/federation/delivery-queue.ts +222 -0
  30. package/references/agent-inbox/src/federation/index.ts +6 -0
  31. package/references/agent-inbox/src/federation/routing-engine.ts +188 -0
  32. package/references/agent-inbox/src/federation/trust.ts +71 -0
  33. package/references/agent-inbox/src/index.ts +390 -0
  34. package/references/agent-inbox/src/ipc/ipc-server.ts +207 -0
  35. package/references/agent-inbox/src/jsonrpc/mail-server.ts +382 -0
  36. package/references/agent-inbox/src/map/map-client.ts +414 -0
  37. package/references/agent-inbox/src/mcp/mcp-server.ts +272 -0
  38. package/references/agent-inbox/src/mesh/delivery-bridge.ts +110 -0
  39. package/references/agent-inbox/src/mesh/mesh-connector.ts +41 -0
  40. package/references/agent-inbox/src/mesh/mesh-transport.ts +157 -0
  41. package/references/agent-inbox/src/mesh/type-mapper.ts +239 -0
  42. package/references/agent-inbox/src/push/notifier.ts +233 -0
  43. package/references/agent-inbox/src/registry/warm-registry.ts +255 -0
  44. package/references/agent-inbox/src/router/message-router.ts +175 -0
  45. package/references/agent-inbox/src/storage/interface.ts +48 -0
  46. package/references/agent-inbox/src/storage/memory.ts +145 -0
  47. package/references/agent-inbox/src/storage/sqlite.ts +671 -0
  48. package/references/agent-inbox/src/traceability/traceability.ts +183 -0
  49. package/references/agent-inbox/src/types.ts +303 -0
  50. package/references/agent-inbox/test/federation/address.test.ts +101 -0
  51. package/references/agent-inbox/test/federation/connection-manager.test.ts +546 -0
  52. package/references/agent-inbox/test/federation/delivery-queue.test.ts +159 -0
  53. package/references/agent-inbox/test/federation/integration.test.ts +857 -0
  54. package/references/agent-inbox/test/federation/routing-engine.test.ts +117 -0
  55. package/references/agent-inbox/test/federation/sdk-integration.test.ts +744 -0
  56. package/references/agent-inbox/test/federation/trust.test.ts +89 -0
  57. package/references/agent-inbox/test/ipc-jsonrpc.test.ts +113 -0
  58. package/references/agent-inbox/test/ipc-server.test.ts +197 -0
  59. package/references/agent-inbox/test/mail-server.test.ts +285 -0
  60. package/references/agent-inbox/test/map-client.test.ts +408 -0
  61. package/references/agent-inbox/test/mesh/delivery-bridge.test.ts +178 -0
  62. package/references/agent-inbox/test/mesh/e2e-mesh.test.ts +527 -0
  63. package/references/agent-inbox/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  64. package/references/agent-inbox/test/mesh/federation-mesh.test.ts +269 -0
  65. package/references/agent-inbox/test/mesh/mesh-connector.test.ts +66 -0
  66. package/references/agent-inbox/test/mesh/mesh-transport.test.ts +191 -0
  67. package/references/agent-inbox/test/mesh/meshpeer-integration.test.ts +442 -0
  68. package/references/agent-inbox/test/mesh/mock-mesh.ts +125 -0
  69. package/references/agent-inbox/test/mesh/mock-meshpeer.ts +266 -0
  70. package/references/agent-inbox/test/mesh/type-mapper.test.ts +226 -0
  71. package/references/agent-inbox/test/message-router.test.ts +184 -0
  72. package/references/agent-inbox/test/push-notifier.test.ts +139 -0
  73. package/references/agent-inbox/test/registry/warm-registry.test.ts +171 -0
  74. package/references/agent-inbox/test/sqlite-prefix.test.ts +192 -0
  75. package/references/agent-inbox/test/sqlite-storage.test.ts +243 -0
  76. package/references/agent-inbox/test/storage.test.ts +196 -0
  77. package/references/agent-inbox/test/traceability.test.ts +123 -0
  78. package/references/agent-inbox/test/wake.test.ts +330 -0
  79. package/references/agent-inbox/tsconfig.json +20 -0
  80. package/references/agent-inbox/tsup.config.ts +10 -0
  81. package/references/agent-inbox/vitest.config.ts +8 -0
  82. package/references/minimem/.claude/settings.json +7 -0
  83. package/references/minimem/.sudocode/issues.jsonl +18 -0
  84. package/references/minimem/.sudocode/specs.jsonl +1 -0
  85. package/references/minimem/CLAUDE.md +329 -0
  86. package/references/minimem/README.md +565 -0
  87. package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
  88. package/references/minimem/claude-plugin/.mcp.json +7 -0
  89. package/references/minimem/claude-plugin/README.md +158 -0
  90. package/references/minimem/claude-plugin/commands/recall.md +47 -0
  91. package/references/minimem/claude-plugin/commands/remember.md +41 -0
  92. package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
  93. package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
  94. package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
  95. package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
  96. package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
  97. package/references/minimem/media/banner.png +0 -0
  98. package/references/minimem/package-lock.json +5373 -0
  99. package/references/minimem/package.json +76 -0
  100. package/references/minimem/scripts/postbuild.js +49 -0
  101. package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
  102. package/references/minimem/src/__tests__/errors.test.ts +265 -0
  103. package/references/minimem/src/__tests__/helpers.ts +199 -0
  104. package/references/minimem/src/__tests__/internal.test.ts +407 -0
  105. package/references/minimem/src/__tests__/knowledge-frontmatter.test.ts +148 -0
  106. package/references/minimem/src/__tests__/knowledge.test.ts +148 -0
  107. package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
  108. package/references/minimem/src/__tests__/session.test.ts +190 -0
  109. package/references/minimem/src/cli/__tests__/commands.test.ts +760 -0
  110. package/references/minimem/src/cli/__tests__/contained-layout.test.ts +286 -0
  111. package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
  112. package/references/minimem/src/cli/commands/append.ts +76 -0
  113. package/references/minimem/src/cli/commands/config.ts +262 -0
  114. package/references/minimem/src/cli/commands/conflicts.ts +415 -0
  115. package/references/minimem/src/cli/commands/daemon.ts +169 -0
  116. package/references/minimem/src/cli/commands/index.ts +12 -0
  117. package/references/minimem/src/cli/commands/init.ts +166 -0
  118. package/references/minimem/src/cli/commands/mcp.ts +221 -0
  119. package/references/minimem/src/cli/commands/push-pull.ts +213 -0
  120. package/references/minimem/src/cli/commands/search.ts +223 -0
  121. package/references/minimem/src/cli/commands/status.ts +84 -0
  122. package/references/minimem/src/cli/commands/store.ts +189 -0
  123. package/references/minimem/src/cli/commands/sync-init.ts +290 -0
  124. package/references/minimem/src/cli/commands/sync.ts +70 -0
  125. package/references/minimem/src/cli/commands/upsert.ts +197 -0
  126. package/references/minimem/src/cli/config.ts +611 -0
  127. package/references/minimem/src/cli/index.ts +299 -0
  128. package/references/minimem/src/cli/shared.ts +189 -0
  129. package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
  130. package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
  131. package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
  132. package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
  133. package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
  134. package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
  135. package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
  136. package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
  137. package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
  138. package/references/minimem/src/cli/sync/central.ts +292 -0
  139. package/references/minimem/src/cli/sync/conflicts.ts +205 -0
  140. package/references/minimem/src/cli/sync/daemon.ts +407 -0
  141. package/references/minimem/src/cli/sync/detection.ts +138 -0
  142. package/references/minimem/src/cli/sync/index.ts +107 -0
  143. package/references/minimem/src/cli/sync/operations.ts +373 -0
  144. package/references/minimem/src/cli/sync/registry.ts +279 -0
  145. package/references/minimem/src/cli/sync/state.ts +358 -0
  146. package/references/minimem/src/cli/sync/validation.ts +206 -0
  147. package/references/minimem/src/cli/sync/watcher.ts +237 -0
  148. package/references/minimem/src/cli/version.ts +34 -0
  149. package/references/minimem/src/core/index.ts +9 -0
  150. package/references/minimem/src/core/indexer.ts +628 -0
  151. package/references/minimem/src/core/searcher.ts +221 -0
  152. package/references/minimem/src/db/schema.ts +183 -0
  153. package/references/minimem/src/db/sqlite-vec.ts +24 -0
  154. package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
  155. package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
  156. package/references/minimem/src/embeddings/batch-openai.ts +409 -0
  157. package/references/minimem/src/embeddings/embeddings.ts +434 -0
  158. package/references/minimem/src/index.ts +132 -0
  159. package/references/minimem/src/internal.ts +299 -0
  160. package/references/minimem/src/minimem.ts +1291 -0
  161. package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
  162. package/references/minimem/src/search/graph.ts +234 -0
  163. package/references/minimem/src/search/hybrid.ts +151 -0
  164. package/references/minimem/src/search/search.ts +256 -0
  165. package/references/minimem/src/server/__tests__/mcp.test.ts +347 -0
  166. package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
  167. package/references/minimem/src/server/mcp.ts +326 -0
  168. package/references/minimem/src/server/tools.ts +720 -0
  169. package/references/minimem/src/session.ts +460 -0
  170. package/references/minimem/src/store/__tests__/manifest.test.ts +177 -0
  171. package/references/minimem/src/store/__tests__/materialize.test.ts +52 -0
  172. package/references/minimem/src/store/__tests__/store-graph.test.ts +228 -0
  173. package/references/minimem/src/store/index.ts +27 -0
  174. package/references/minimem/src/store/manifest.ts +203 -0
  175. package/references/minimem/src/store/materialize.ts +185 -0
  176. package/references/minimem/src/store/store-graph.ts +252 -0
  177. package/references/minimem/tsconfig.json +19 -0
  178. package/references/minimem/tsup.config.ts +26 -0
  179. package/references/minimem/vitest.config.ts +29 -0
  180. package/references/openteams/src/cli/generate.ts +23 -1
  181. package/references/openteams/src/generators/agent-prompt-generator.test.ts +94 -0
  182. package/references/openteams/src/generators/agent-prompt-generator.ts +42 -13
  183. package/references/openteams/src/generators/package-generator.ts +9 -1
  184. package/references/openteams/src/generators/skill-generator.test.ts +28 -0
  185. package/references/openteams/src/generators/skill-generator.ts +10 -4
  186. package/references/skill-tree/.claude/settings.json +6 -0
  187. package/references/skill-tree/.sudocode/issues.jsonl +19 -0
  188. package/references/skill-tree/.sudocode/specs.jsonl +3 -0
  189. package/references/skill-tree/CLAUDE.md +132 -0
  190. package/references/skill-tree/README.md +396 -0
  191. package/references/skill-tree/docs/GAPS_v1.md +221 -0
  192. package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
  193. package/references/skill-tree/docs/TODOS.md +91 -0
  194. package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
  195. package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
  196. package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
  197. package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
  198. package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
  199. package/references/skill-tree/docs/scraper/README.md +170 -0
  200. package/references/skill-tree/examples/basic-usage.ts +157 -0
  201. package/references/skill-tree/package-lock.json +1852 -0
  202. package/references/skill-tree/package.json +66 -0
  203. package/references/skill-tree/plan.md +78 -0
  204. package/references/skill-tree/scraper/README.md +123 -0
  205. package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
  206. package/references/skill-tree/scraper/docs/PLAN.md +336 -0
  207. package/references/skill-tree/scraper/drizzle.config.ts +10 -0
  208. package/references/skill-tree/scraper/package-lock.json +6329 -0
  209. package/references/skill-tree/scraper/package.json +68 -0
  210. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
  211. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
  212. package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
  213. package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
  214. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
  215. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
  216. package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
  217. package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
  218. package/references/skill-tree/scraper/tsup.config.ts +14 -0
  219. package/references/skill-tree/scraper/vitest.config.ts +17 -0
  220. package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
  221. package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
  222. package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
  223. package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
  224. package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
  225. package/references/skill-tree/test/run-all.ts +106 -0
  226. package/references/skill-tree/test/utils.ts +128 -0
  227. package/references/skill-tree/vitest.config.ts +16 -0
  228. package/references/swarmkit/src/commands/init/phases/configure.ts +0 -22
  229. package/references/swarmkit/src/commands/init/phases/global-setup.ts +5 -3
  230. package/references/swarmkit/src/commands/init/wizard.ts +2 -2
  231. package/references/swarmkit/src/packages/setup.test.ts +53 -7
  232. package/references/swarmkit/src/packages/setup.ts +37 -1
  233. package/scripts/bootstrap.mjs +26 -1
  234. package/scripts/generate-agents.mjs +5 -1
  235. package/scripts/map-hook.mjs +97 -64
  236. package/scripts/map-sidecar.mjs +179 -25
  237. package/scripts/team-loader.mjs +12 -41
  238. package/skills/swarm/SKILL.md +89 -25
  239. package/src/__tests__/agent-generator.test.mjs +6 -13
  240. package/src/__tests__/bootstrap.test.mjs +124 -1
  241. package/src/__tests__/config.test.mjs +200 -27
  242. package/src/__tests__/e2e-live-map.test.mjs +536 -0
  243. package/src/__tests__/e2e-mesh-sidecar.test.mjs +570 -0
  244. package/src/__tests__/e2e-native-task-hooks.test.mjs +376 -0
  245. package/src/__tests__/e2e-sidecar-bridge.test.mjs +477 -0
  246. package/src/__tests__/helpers.mjs +13 -0
  247. package/src/__tests__/inbox.test.mjs +22 -89
  248. package/src/__tests__/index.test.mjs +35 -9
  249. package/src/__tests__/integration.test.mjs +513 -0
  250. package/src/__tests__/map-events.test.mjs +514 -150
  251. package/src/__tests__/mesh-connection.test.mjs +308 -0
  252. package/src/__tests__/opentasks-client.test.mjs +517 -0
  253. package/src/__tests__/paths.test.mjs +185 -41
  254. package/src/__tests__/sidecar-client.test.mjs +35 -0
  255. package/src/__tests__/sidecar-server.test.mjs +124 -0
  256. package/src/__tests__/skilltree-client.test.mjs +80 -0
  257. package/src/agent-generator.mjs +104 -33
  258. package/src/bootstrap.mjs +150 -10
  259. package/src/config.mjs +81 -17
  260. package/src/context-output.mjs +58 -8
  261. package/src/inbox.mjs +9 -54
  262. package/src/index.mjs +39 -8
  263. package/src/map-connection.mjs +4 -3
  264. package/src/map-events.mjs +350 -80
  265. package/src/mesh-connection.mjs +148 -0
  266. package/src/opentasks-client.mjs +269 -0
  267. package/src/paths.mjs +182 -27
  268. package/src/sessionlog.mjs +14 -9
  269. package/src/sidecar-client.mjs +81 -27
  270. package/src/sidecar-server.mjs +175 -16
  271. package/src/skilltree-client.mjs +173 -0
  272. package/src/template.mjs +68 -4
  273. package/vitest.config.mjs +1 -0
@@ -0,0 +1,900 @@
1
+ /**
2
+ * Tier 5: Sidecar + Inbox Integration Tests
3
+ *
4
+ * Real sidecar process, real UNIX sockets, mock MAP server.
5
+ * No LLM calls — exercises the full sidecar/hook/inbox pipeline directly.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { spawn } from "child_process";
12
+ import { fileURLToPath } from "url";
13
+ import { MockMapServer } from "./helpers/map-mock-server.mjs";
14
+ import { startTestSidecar, sendCommand, isProcessAlive } from "./helpers/sidecar.mjs";
15
+ import { createWorkspace } from "./helpers/workspace.mjs";
16
+ import { waitFor } from "./helpers/cleanup.mjs";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const PLUGIN_DIR = path.resolve(__dirname, "..");
20
+ const HOOK_SCRIPT = path.join(PLUGIN_DIR, "scripts", "map-hook.mjs");
21
+
22
+ // Use /tmp/ for short socket paths (macOS limits Unix socket paths to 104 bytes)
23
+ const SHORT_TMPDIR = "/tmp";
24
+
25
+ // Check if agent-inbox is available
26
+ let agentInboxAvailable = false;
27
+ try {
28
+ await import("agent-inbox");
29
+ agentInboxAvailable = true;
30
+ } catch {
31
+ // Not installed
32
+ }
33
+
34
+ /**
35
+ * Run a hook script with stdin data and return stdout + stderr.
36
+ */
37
+ function runHook(action, stdinData, cwd, env = {}) {
38
+ return new Promise((resolve) => {
39
+ const child = spawn("node", [HOOK_SCRIPT, action], {
40
+ cwd,
41
+ stdio: ["pipe", "pipe", "pipe"],
42
+ env: { ...process.env, ...env },
43
+ });
44
+
45
+ let stdout = "";
46
+ let stderr = "";
47
+ child.stdout.on("data", (d) => (stdout += d.toString()));
48
+ child.stderr.on("data", (d) => (stderr += d.toString()));
49
+
50
+ child.stdin.write(JSON.stringify(stdinData));
51
+ child.stdin.end();
52
+
53
+ child.on("close", (code) => {
54
+ resolve({ code, stdout, stderr });
55
+ });
56
+
57
+ setTimeout(() => {
58
+ child.kill();
59
+ resolve({ code: -1, stdout, stderr });
60
+ }, 15000);
61
+ });
62
+ }
63
+
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ // Group 1: Lifecycle Socket Tests
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+
68
+ describe("tier5: sidecar lifecycle", { timeout: 60_000 }, () => {
69
+ let mockServer;
70
+ let workspace;
71
+ let sidecar;
72
+
73
+ beforeAll(async () => {
74
+ mockServer = new MockMapServer();
75
+ await mockServer.start();
76
+ });
77
+
78
+ afterAll(async () => {
79
+ await mockServer.stop();
80
+ });
81
+
82
+ afterEach(async () => {
83
+ if (sidecar) {
84
+ sidecar.cleanup();
85
+ sidecar = null;
86
+ }
87
+ mockServer.clearMessages();
88
+ if (workspace) {
89
+ workspace.cleanup();
90
+ workspace = null;
91
+ }
92
+ });
93
+
94
+ it("starts and responds to ping", async () => {
95
+ workspace = createWorkspace({
96
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
97
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
98
+ });
99
+ sidecar = await startTestSidecar({
100
+ workspaceDir: workspace.dir,
101
+ mockServerPort: mockServer.port,
102
+ });
103
+
104
+ const resp = await sendCommand(sidecar.socketPath, { action: "ping" });
105
+ expect(resp.ok).toBe(true);
106
+ expect(resp.pid).toBeGreaterThan(0);
107
+ });
108
+
109
+ it("mock server receives connection from sidecar", async () => {
110
+ workspace = createWorkspace({
111
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
112
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
113
+ });
114
+ sidecar = await startTestSidecar({
115
+ workspaceDir: workspace.dir,
116
+ mockServerPort: mockServer.port,
117
+ });
118
+
119
+ expect(mockServer.connections.length).toBeGreaterThan(0);
120
+ // Should have seen map/connect and map/agents/register
121
+ const connectMsgs = mockServer.getByMethod("map/connect");
122
+ const registerMsgs = mockServer.getByMethod("map/agents/register");
123
+ expect(connectMsgs.length).toBeGreaterThan(0);
124
+ expect(registerMsgs.length).toBeGreaterThan(0);
125
+ });
126
+
127
+ it("spawn -> done round-trip", async () => {
128
+ workspace = createWorkspace({
129
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
130
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
131
+ });
132
+ sidecar = await startTestSidecar({
133
+ workspaceDir: workspace.dir,
134
+ mockServerPort: mockServer.port,
135
+ });
136
+
137
+ // Spawn
138
+ const spawnResp = await sendCommand(sidecar.socketPath, {
139
+ action: "spawn",
140
+ agent: {
141
+ agentId: "tu_123/coordinator",
142
+ name: "coordinator",
143
+ role: "coordinator",
144
+ scopes: ["swarm:test"],
145
+ metadata: { template: "gsd" },
146
+ },
147
+ });
148
+ expect(spawnResp.ok).toBe(true);
149
+ expect(spawnResp.agent).toBeDefined();
150
+
151
+ // Verify mock received spawn
152
+ expect(mockServer.spawnedAgents.length).toBe(1);
153
+ expect(mockServer.spawnedAgents[0].agentId).toBe("tu_123/coordinator");
154
+
155
+ // Done
156
+ const doneResp = await sendCommand(sidecar.socketPath, {
157
+ action: "done",
158
+ agentId: "tu_123/coordinator",
159
+ reason: "completed",
160
+ });
161
+ expect(doneResp.ok).toBe(true);
162
+
163
+ // Verify mock received unregister for our specific agent
164
+ const unregisters = mockServer.callExtensions.filter(
165
+ (e) => e.method === "map/agents/unregister" && e.params?.agentId === "tu_123/coordinator"
166
+ );
167
+ expect(unregisters.length).toBe(1);
168
+ });
169
+
170
+ it("state update reaches mock server", async () => {
171
+ workspace = createWorkspace({
172
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
173
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
174
+ });
175
+ sidecar = await startTestSidecar({
176
+ workspaceDir: workspace.dir,
177
+ mockServerPort: mockServer.port,
178
+ });
179
+
180
+ const resp = await sendCommand(sidecar.socketPath, {
181
+ action: "state",
182
+ state: "busy",
183
+ metadata: { lastStopReason: "tool_use" },
184
+ });
185
+ expect(resp.ok).toBe(true);
186
+
187
+ // Verify mock received state update
188
+ await waitFor(() => mockServer.stateUpdates.length > 0, 3000);
189
+ expect(mockServer.stateUpdates.length).toBeGreaterThan(0);
190
+ });
191
+
192
+ it("trajectory checkpoint with fallback", async () => {
193
+ workspace = createWorkspace({
194
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
195
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
196
+ });
197
+ sidecar = await startTestSidecar({
198
+ workspaceDir: workspace.dir,
199
+ mockServerPort: mockServer.port,
200
+ });
201
+
202
+ // trajectory not supported → falls back to broadcast
203
+ mockServer.trajectorySupported = false;
204
+ const resp = await sendCommand(sidecar.socketPath, {
205
+ action: "trajectory-checkpoint",
206
+ checkpoint: {
207
+ id: "cp1",
208
+ agentId: "test-agent",
209
+ sessionId: "test-session",
210
+ label: "test checkpoint",
211
+ metadata: { phase: "active" },
212
+ },
213
+ });
214
+ expect(resp.ok).toBe(true);
215
+ expect(resp.method).toBe("broadcast-fallback");
216
+
217
+ // Verify fallback sent via map/send with trajectory.checkpoint payload
218
+ const trajectoryMessages = mockServer.sentMessages.filter(
219
+ (m) => m.payload?.type === "trajectory.checkpoint"
220
+ );
221
+ expect(trajectoryMessages.length).toBe(1);
222
+
223
+ // Now test with trajectory supported
224
+ mockServer.trajectorySupported = true;
225
+ mockServer.clearMessages();
226
+ const resp2 = await sendCommand(sidecar.socketPath, {
227
+ action: "trajectory-checkpoint",
228
+ checkpoint: { id: "cp2", agentId: "a", sessionId: "s", label: "l", metadata: {} },
229
+ });
230
+ expect(resp2.ok).toBe(true);
231
+ expect(resp2.method).toBe("trajectory");
232
+ });
233
+
234
+ it("emit payload reaches mock server", async () => {
235
+ workspace = createWorkspace({
236
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
237
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
238
+ });
239
+ sidecar = await startTestSidecar({
240
+ workspaceDir: workspace.dir,
241
+ mockServerPort: mockServer.port,
242
+ });
243
+
244
+ const resp = await sendCommand(sidecar.socketPath, {
245
+ action: "emit",
246
+ event: { type: "task.dispatched", taskId: "t1", targetAgent: "tu_abc/researcher" },
247
+ });
248
+ expect(resp.ok).toBe(true);
249
+
250
+ await waitFor(() => mockServer.sentMessages.length > 0, 3000);
251
+ expect(mockServer.sentMessages.length).toBe(1);
252
+ expect(mockServer.sentMessages[0].payload?.type).toBe("task.dispatched");
253
+ });
254
+
255
+ it("multiple agents lifecycle", async () => {
256
+ workspace = createWorkspace({
257
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
258
+ config: { template: "gsd", map: { enabled: true, server: `ws://localhost:${mockServer.port}` } },
259
+ });
260
+ sidecar = await startTestSidecar({
261
+ workspaceDir: workspace.dir,
262
+ mockServerPort: mockServer.port,
263
+ });
264
+
265
+ // Spawn 3 agents
266
+ const agents = [
267
+ { agentId: "tu_1/coordinator", name: "coordinator", role: "coordinator" },
268
+ { agentId: "tu_2/researcher", name: "researcher", role: "researcher" },
269
+ { agentId: "tu_3/executor", name: "executor", role: "executor" },
270
+ ];
271
+
272
+ for (const agent of agents) {
273
+ const resp = await sendCommand(sidecar.socketPath, {
274
+ action: "spawn",
275
+ agent: { ...agent, scopes: ["swarm:test"], metadata: {} },
276
+ });
277
+ expect(resp.ok).toBe(true);
278
+ }
279
+ expect(mockServer.spawnedAgents.length).toBe(3);
280
+
281
+ // State updates
282
+ for (const agent of agents) {
283
+ await sendCommand(sidecar.socketPath, {
284
+ action: "state",
285
+ state: "busy",
286
+ agentId: agent.agentId,
287
+ });
288
+ }
289
+
290
+ // Done in sequence
291
+ for (const agent of agents) {
292
+ const resp = await sendCommand(sidecar.socketPath, {
293
+ action: "done",
294
+ agentId: agent.agentId,
295
+ reason: "completed",
296
+ });
297
+ expect(resp.ok).toBe(true);
298
+ }
299
+
300
+ const agentIds = agents.map((a) => a.agentId);
301
+ const unregisters = mockServer.callExtensions.filter(
302
+ (e) => e.method === "map/agents/unregister" && agentIds.includes(e.params?.agentId)
303
+ );
304
+ expect(unregisters.length).toBe(3);
305
+ });
306
+ });
307
+
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+ // Group 2: Inbox Socket Tests (requires agent-inbox)
310
+ // ─────────────────────────────────────────────────────────────────────────────
311
+
312
+ describe.skipIf(!agentInboxAvailable)(
313
+ "tier5: inbox integration",
314
+ { timeout: 60_000 },
315
+ () => {
316
+ let mockServer;
317
+ let workspace;
318
+ let sidecar;
319
+
320
+ beforeAll(async () => {
321
+ mockServer = new MockMapServer();
322
+ await mockServer.start();
323
+ });
324
+
325
+ afterAll(async () => {
326
+ await mockServer.stop();
327
+ });
328
+
329
+ afterEach(async () => {
330
+ if (sidecar) {
331
+ sidecar.cleanup();
332
+ sidecar = null;
333
+ }
334
+ mockServer.clearMessages();
335
+ if (workspace) {
336
+ workspace.cleanup();
337
+ workspace = null;
338
+ }
339
+ });
340
+
341
+ it("starts with both sockets when inbox configured", async () => {
342
+ workspace = createWorkspace({
343
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
344
+ config: {
345
+ template: "gsd",
346
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
347
+ inbox: { enabled: true },
348
+ },
349
+ });
350
+ sidecar = await startTestSidecar({
351
+ workspaceDir: workspace.dir,
352
+ mockServerPort: mockServer.port,
353
+ inboxConfig: { enabled: true },
354
+ });
355
+
356
+ expect(fs.existsSync(sidecar.socketPath)).toBe(true);
357
+ expect(sidecar.inboxReady).toBe(true);
358
+ expect(fs.existsSync(sidecar.inboxSocketPath)).toBe(true);
359
+ });
360
+
361
+ it("starts with only lifecycle socket when inbox not configured", async () => {
362
+ workspace = createWorkspace({
363
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
364
+ config: {
365
+ template: "gsd",
366
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
367
+ },
368
+ });
369
+ sidecar = await startTestSidecar({
370
+ workspaceDir: workspace.dir,
371
+ mockServerPort: mockServer.port,
372
+ // No inboxConfig
373
+ });
374
+
375
+ expect(fs.existsSync(sidecar.socketPath)).toBe(true);
376
+ expect(fs.existsSync(sidecar.inboxSocketPath)).toBe(false);
377
+ });
378
+
379
+ it("check_inbox returns empty initially", async () => {
380
+ workspace = createWorkspace({
381
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
382
+ config: {
383
+ template: "gsd",
384
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
385
+ inbox: { enabled: true },
386
+ },
387
+ });
388
+ sidecar = await startTestSidecar({
389
+ workspaceDir: workspace.dir,
390
+ mockServerPort: mockServer.port,
391
+ inboxConfig: { enabled: true },
392
+ });
393
+
394
+ const resp = await sendCommand(sidecar.inboxSocketPath, {
395
+ action: "check_inbox",
396
+ scope: "swarm:test",
397
+ });
398
+ expect(resp).not.toBeNull();
399
+ expect(resp.ok).toBe(true);
400
+ expect(resp.messages).toEqual([]);
401
+ });
402
+
403
+ it("message sent via IPC appears in check_inbox", async () => {
404
+ workspace = createWorkspace({
405
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
406
+ config: {
407
+ template: "gsd",
408
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
409
+ inbox: { enabled: true },
410
+ },
411
+ });
412
+ sidecar = await startTestSidecar({
413
+ workspaceDir: workspace.dir,
414
+ mockServerPort: mockServer.port,
415
+ inboxConfig: { enabled: true },
416
+ });
417
+
418
+ // Send a message via agent-inbox IPC (the `send` action properly sets recipients)
419
+ const sendResp = await sendCommand(sidecar.inboxSocketPath, {
420
+ action: "send",
421
+ from: "ext-agent-1",
422
+ to: "swarm:test",
423
+ scope: "swarm:test",
424
+ payload: { type: "text", text: "Hello from external agent" },
425
+ });
426
+ expect(sendResp?.ok).toBe(true);
427
+
428
+ // Now check_inbox should find the message
429
+ let resp;
430
+ const found = await waitFor(async () => {
431
+ resp = await sendCommand(sidecar.inboxSocketPath, {
432
+ action: "check_inbox",
433
+ scope: "swarm:test",
434
+ });
435
+ return resp?.ok && resp?.messages?.length > 0;
436
+ }, 5000);
437
+
438
+ expect(found).toBe(true);
439
+ expect(resp.messages.length).toBeGreaterThan(0);
440
+
441
+ // Verify sidecar is still alive
442
+ expect(isProcessAlive(sidecar.pid)).toBe(true);
443
+ });
444
+
445
+ it("inbound MAP message appears in check_inbox via notification path", async () => {
446
+ workspace = createWorkspace({
447
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
448
+ config: {
449
+ template: "gsd",
450
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
451
+ inbox: { enabled: true },
452
+ },
453
+ });
454
+ sidecar = await startTestSidecar({
455
+ workspaceDir: workspace.dir,
456
+ mockServerPort: mockServer.port,
457
+ inboxConfig: { enabled: true },
458
+ });
459
+
460
+ // Send a MAP message via mock server → SDK onMessage → agent-inbox handleIncoming
461
+ mockServer.sendToAll(
462
+ { type: "text", text: "Hello via MAP notification" },
463
+ { from: "remote-agent-1", to: { scope: "swarm:test" } }
464
+ );
465
+
466
+ // Poll check_inbox — agent-inbox 0.1.3's resolveRecipients should set
467
+ // recipients from the `to` field so the message is findable
468
+ let resp;
469
+ const found = await waitFor(async () => {
470
+ resp = await sendCommand(sidecar.inboxSocketPath, {
471
+ action: "check_inbox",
472
+ scope: "swarm:test",
473
+ });
474
+ return resp?.ok && resp?.messages?.length > 0;
475
+ }, 8000);
476
+
477
+ expect(found).toBe(true);
478
+ expect(resp.messages.length).toBe(1);
479
+ const msg = resp.messages[0];
480
+ expect(msg.sender_id).toBe("remote-agent-1");
481
+ // Content should contain the original payload
482
+ const text = msg.content?.text || (typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content));
483
+ expect(text).toContain("Hello via MAP notification");
484
+ });
485
+
486
+ it("multiple MAP messages from different senders appear in check_inbox", async () => {
487
+ workspace = createWorkspace({
488
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
489
+ config: {
490
+ template: "gsd",
491
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
492
+ inbox: { enabled: true },
493
+ },
494
+ });
495
+ sidecar = await startTestSidecar({
496
+ workspaceDir: workspace.dir,
497
+ mockServerPort: mockServer.port,
498
+ inboxConfig: { enabled: true },
499
+ });
500
+
501
+ // Send 3 messages from different external agents
502
+ mockServer.sendToAll(
503
+ { type: "text", text: "msg-1" },
504
+ { from: "agent-alpha", to: { scope: "swarm:test" } }
505
+ );
506
+ mockServer.sendToAll(
507
+ { type: "text", text: "msg-2" },
508
+ { from: "agent-beta", to: { scope: "swarm:test" } }
509
+ );
510
+ mockServer.sendToAll(
511
+ { type: "text", text: "msg-3" },
512
+ { from: "agent-gamma", to: { scope: "swarm:test" } }
513
+ );
514
+
515
+ // Wait for all 3 messages to arrive
516
+ let resp;
517
+ const found = await waitFor(async () => {
518
+ resp = await sendCommand(sidecar.inboxSocketPath, {
519
+ action: "check_inbox",
520
+ scope: "swarm:test",
521
+ });
522
+ return resp?.ok && resp?.messages?.length >= 3;
523
+ }, 8000);
524
+
525
+ expect(found).toBe(true);
526
+ expect(resp.messages.length).toBe(3);
527
+ const senders = resp.messages.map((m) => m.sender_id).sort();
528
+ expect(senders).toEqual(["agent-alpha", "agent-beta", "agent-gamma"]);
529
+ });
530
+
531
+ it("MAP message addressed by agentId appears in check_inbox", async () => {
532
+ workspace = createWorkspace({
533
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
534
+ config: {
535
+ template: "gsd",
536
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
537
+ inbox: { enabled: true },
538
+ },
539
+ });
540
+ sidecar = await startTestSidecar({
541
+ workspaceDir: workspace.dir,
542
+ mockServerPort: mockServer.port,
543
+ inboxConfig: { enabled: true },
544
+ });
545
+
546
+ // Send with to: { agentId: "my-agent" } addressing
547
+ mockServer.sendToAll(
548
+ { type: "text", text: "direct message" },
549
+ { from: "sender-1", to: { agentId: "my-agent" } }
550
+ );
551
+
552
+ let resp;
553
+ const found = await waitFor(async () => {
554
+ resp = await sendCommand(sidecar.inboxSocketPath, {
555
+ action: "check_inbox",
556
+ scope: "my-agent",
557
+ });
558
+ return resp?.ok && resp?.messages?.length > 0;
559
+ }, 8000);
560
+
561
+ expect(found).toBe(true);
562
+ expect(resp.messages[0].sender_id).toBe("sender-1");
563
+ });
564
+
565
+ it("inject hook surfaces MAP-delivered messages as markdown", async () => {
566
+ workspace = createWorkspace({
567
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
568
+ config: {
569
+ template: "gsd",
570
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}`, sidecar: "session" },
571
+ inbox: { enabled: true },
572
+ },
573
+ });
574
+
575
+ const mapDir = path.join(workspace.dir, ".swarm", "claude-swarm", "tmp", "map");
576
+ fs.mkdirSync(mapDir, { recursive: true });
577
+
578
+ sidecar = await startTestSidecar({
579
+ workspaceDir: workspace.dir,
580
+ mockServerPort: mockServer.port,
581
+ inboxConfig: { enabled: true },
582
+ });
583
+
584
+ // Send via MAP notification path (not IPC)
585
+ mockServer.sendToAll(
586
+ { type: "text", text: "Urgent coordination update" },
587
+ { from: "map-coordinator", to: { scope: "default" } }
588
+ );
589
+
590
+ // Wait for agent-inbox to store it
591
+ await waitFor(async () => {
592
+ const resp = await sendCommand(sidecar.inboxSocketPath, {
593
+ action: "check_inbox",
594
+ scope: "default",
595
+ });
596
+ return resp?.ok && resp?.messages?.length > 0;
597
+ }, 8000);
598
+
599
+ // Run inject hook — should read from agent-inbox IPC and output markdown
600
+ const result = await runHook("inject", { session_id: "" }, workspace.dir);
601
+ expect(result.code).toBe(0);
602
+ expect(result.stdout).toContain("[MAP]");
603
+ expect(result.stdout).toContain("map-coordinator");
604
+ });
605
+
606
+ it("implicit inbox enablement via MAP config only", async () => {
607
+ // Config has only map.server — inbox should be implicitly enabled
608
+ workspace = createWorkspace({
609
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
610
+ config: {
611
+ template: "gsd",
612
+ map: { server: `ws://localhost:${mockServer.port}` },
613
+ // No explicit inbox config
614
+ },
615
+ });
616
+ sidecar = await startTestSidecar({
617
+ workspaceDir: workspace.dir,
618
+ mockServerPort: mockServer.port,
619
+ inboxConfig: { enabled: true }, // Sidecar still needs the flag
620
+ });
621
+
622
+ expect(fs.existsSync(sidecar.socketPath)).toBe(true);
623
+ expect(sidecar.inboxReady).toBe(true);
624
+ });
625
+ }
626
+ );
627
+
628
+ // ─────────────────────────────────────────────────────────────────────────────
629
+ // Group 3: Resilience Tests
630
+ // ─────────────────────────────────────────────────────────────────────────────
631
+
632
+ describe("tier5: resilience", { timeout: 90_000 }, () => {
633
+ let mockServer;
634
+ let workspace;
635
+ let sidecar;
636
+
637
+ beforeAll(async () => {
638
+ mockServer = new MockMapServer();
639
+ await mockServer.start();
640
+ });
641
+
642
+ afterAll(async () => {
643
+ mockServer.setResponseDelay(0);
644
+ await mockServer.stop();
645
+ });
646
+
647
+ afterEach(async () => {
648
+ mockServer.setResponseDelay(0);
649
+ if (sidecar) {
650
+ sidecar.cleanup();
651
+ sidecar = null;
652
+ }
653
+ mockServer.clearMessages();
654
+ if (workspace) {
655
+ workspace.cleanup();
656
+ workspace = null;
657
+ }
658
+ });
659
+
660
+ it("sidecar starts with null connection when MAP server unavailable", async () => {
661
+ workspace = createWorkspace({
662
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
663
+ config: {
664
+ template: "gsd",
665
+ map: { enabled: true, server: "ws://localhost:1" }, // Nothing listening
666
+ },
667
+ });
668
+
669
+ // Sidecar should still start (catches connection error)
670
+ sidecar = await startTestSidecar({
671
+ workspaceDir: workspace.dir,
672
+ mockServerPort: 1, // Unreachable
673
+ });
674
+
675
+ // Lifecycle socket should work
676
+ const resp = await sendCommand(sidecar.socketPath, { action: "ping" });
677
+ expect(resp.ok).toBe(true);
678
+
679
+ // Spawn should fail gracefully (no connection)
680
+ const spawnResp = await sendCommand(sidecar.socketPath, {
681
+ action: "spawn",
682
+ agent: { agentId: "a", name: "a", role: "r", scopes: [], metadata: {} },
683
+ });
684
+ expect(spawnResp.ok).toBe(false);
685
+ expect(spawnResp.error).toContain("no connection");
686
+ });
687
+
688
+ it("handles delayed MAP server responses", async () => {
689
+ mockServer.setResponseDelay(2000); // 2s delay
690
+
691
+ workspace = createWorkspace({
692
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
693
+ config: {
694
+ template: "gsd",
695
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
696
+ },
697
+ });
698
+
699
+ // Sidecar should still connect (SDK has timeout > 2s)
700
+ sidecar = await startTestSidecar({
701
+ workspaceDir: workspace.dir,
702
+ mockServerPort: mockServer.port,
703
+ });
704
+
705
+ // Commands should work, just slower
706
+ const start = Date.now();
707
+ const resp = await sendCommand(sidecar.socketPath, {
708
+ action: "spawn",
709
+ agent: { agentId: "slow-1", name: "slow", role: "worker", scopes: ["s"], metadata: {} },
710
+ });
711
+ const elapsed = Date.now() - start;
712
+
713
+ expect(resp.ok).toBe(true);
714
+ expect(elapsed).toBeGreaterThan(1500); // Should have been delayed
715
+ });
716
+
717
+ it("inactivity timeout self-terminates sidecar", async () => {
718
+ workspace = createWorkspace({
719
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
720
+ config: {
721
+ template: "gsd",
722
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
723
+ },
724
+ });
725
+ sidecar = await startTestSidecar({
726
+ workspaceDir: workspace.dir,
727
+ mockServerPort: mockServer.port,
728
+ inactivityTimeoutMs: 3000, // 3 seconds
729
+ });
730
+
731
+ const pid = sidecar.pid;
732
+ expect(isProcessAlive(pid)).toBe(true);
733
+
734
+ // Wait for timeout + buffer
735
+ const died = await waitFor(() => !isProcessAlive(pid), 10000);
736
+ expect(died).toBe(true);
737
+ });
738
+
739
+ it("inactivity timer resets on activity", async () => {
740
+ workspace = createWorkspace({
741
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
742
+ config: {
743
+ template: "gsd",
744
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
745
+ },
746
+ });
747
+ sidecar = await startTestSidecar({
748
+ workspaceDir: workspace.dir,
749
+ mockServerPort: mockServer.port,
750
+ inactivityTimeoutMs: 4000,
751
+ });
752
+
753
+ const pid = sidecar.pid;
754
+
755
+ // Send pings at 2s intervals to keep it alive past the 4s timeout
756
+ for (let i = 0; i < 3; i++) {
757
+ await new Promise((r) => setTimeout(r, 2000));
758
+ expect(isProcessAlive(pid)).toBe(true);
759
+ await sendCommand(sidecar.socketPath, { action: "ping" });
760
+ }
761
+
762
+ // Should still be alive after 6s (3 × 2s pings)
763
+ expect(isProcessAlive(pid)).toBe(true);
764
+
765
+ // Now stop pinging — should die after ~4s
766
+ const died = await waitFor(() => !isProcessAlive(pid), 8000);
767
+ expect(died).toBe(true);
768
+ });
769
+
770
+ it("sidecar process is cleanly killable via SIGTERM", async () => {
771
+ workspace = createWorkspace({
772
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
773
+ config: {
774
+ template: "gsd",
775
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
776
+ },
777
+ });
778
+ sidecar = await startTestSidecar({
779
+ workspaceDir: workspace.dir,
780
+ mockServerPort: mockServer.port,
781
+ });
782
+
783
+ const pid = sidecar.pid;
784
+ expect(isProcessAlive(pid)).toBe(true);
785
+
786
+ process.kill(pid, "SIGTERM");
787
+
788
+ const died = await waitFor(() => !isProcessAlive(pid), 5000);
789
+ expect(died).toBe(true);
790
+ });
791
+ });
792
+
793
+ // ─────────────────────────────────────────────────────────────────────────────
794
+ // Group 5: Hook Integration Tests
795
+ // ─────────────────────────────────────────────────────────────────────────────
796
+
797
+ describe("tier5: hook integration", { timeout: 60_000 }, () => {
798
+ let mockServer;
799
+ let workspace;
800
+ let sidecar;
801
+
802
+ beforeAll(async () => {
803
+ mockServer = new MockMapServer();
804
+ await mockServer.start();
805
+ });
806
+
807
+ afterAll(async () => {
808
+ await mockServer.stop();
809
+ });
810
+
811
+ afterEach(async () => {
812
+ if (sidecar) {
813
+ sidecar.cleanup();
814
+ sidecar = null;
815
+ }
816
+ mockServer.clearMessages();
817
+ if (workspace) {
818
+ workspace.cleanup();
819
+ workspace = null;
820
+ }
821
+ });
822
+
823
+ it("turn-completed hook updates state to idle", async () => {
824
+ workspace = createWorkspace({
825
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
826
+ config: {
827
+ template: "gsd",
828
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}`, sidecar: "session" },
829
+ },
830
+ });
831
+
832
+ // Ensure map dir exists
833
+ const mapDir = path.join(workspace.dir, ".swarm", "claude-swarm", "tmp", "map");
834
+ fs.mkdirSync(mapDir, { recursive: true });
835
+
836
+ sidecar = await startTestSidecar({
837
+ workspaceDir: workspace.dir,
838
+ mockServerPort: mockServer.port,
839
+ });
840
+
841
+ const result = await runHook(
842
+ "turn-completed",
843
+ { stop_reason: "end_turn", session_id: "" },
844
+ workspace.dir
845
+ );
846
+ expect(result.code).toBe(0);
847
+
848
+ // Should see a state update
849
+ await waitFor(() => mockServer.stateUpdates.length > 0, 5000);
850
+ expect(mockServer.stateUpdates.length).toBeGreaterThan(0);
851
+ });
852
+
853
+ it("inject hook reads inbox via agent-inbox IPC and outputs markdown", async () => {
854
+ workspace = createWorkspace({
855
+ tmpdir: SHORT_TMPDIR, prefix: "s5-",
856
+ config: {
857
+ template: "gsd",
858
+ map: { enabled: true, server: `ws://localhost:${mockServer.port}`, sidecar: "session" },
859
+ inbox: { enabled: true },
860
+ },
861
+ });
862
+
863
+ const mapDir = path.join(workspace.dir, ".swarm", "claude-swarm", "tmp", "map");
864
+ fs.mkdirSync(mapDir, { recursive: true });
865
+
866
+ sidecar = await startTestSidecar({
867
+ workspaceDir: workspace.dir,
868
+ mockServerPort: mockServer.port,
869
+ inboxConfig: { enabled: true },
870
+ });
871
+
872
+ // Send a message via agent-inbox IPC (properly sets recipients for check_inbox)
873
+ const scope = "default";
874
+ await sendCommand(sidecar.inboxSocketPath, {
875
+ action: "send",
876
+ from: "agent-42",
877
+ to: scope,
878
+ scope,
879
+ payload: { type: "text", text: "Hello from another agent" },
880
+ });
881
+
882
+ // Verify message is in inbox before running the hook
883
+ await waitFor(async () => {
884
+ const resp = await sendCommand(sidecar.inboxSocketPath, {
885
+ action: "check_inbox",
886
+ scope,
887
+ });
888
+ return resp?.ok && resp?.messages?.length > 0;
889
+ }, 5000);
890
+
891
+ // Now run the inject hook — it should read from agent-inbox IPC
892
+ const result = await runHook("inject", { session_id: "" }, workspace.dir);
893
+ expect(result.code).toBe(0);
894
+
895
+ // Should output markdown with the message
896
+ expect(result.stdout).toContain("[MAP]");
897
+ expect(result.stdout).toContain("agent-42");
898
+ });
899
+
900
+ });