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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +22 -1
- package/.claude-plugin/run-agent-inbox-mcp.sh +76 -0
- package/.claude-plugin/run-minimem-mcp.sh +98 -0
- package/.claude-plugin/run-opentasks-mcp.sh +65 -0
- package/CLAUDE.md +200 -36
- package/README.md +65 -0
- package/e2e/helpers/cleanup.mjs +17 -3
- package/e2e/helpers/map-mock-server.mjs +201 -25
- package/e2e/helpers/sidecar.mjs +222 -0
- package/e2e/helpers/workspace.mjs +2 -1
- package/e2e/tier5-sidecar-inbox.test.mjs +900 -0
- package/e2e/tier6-inbox-mcp.test.mjs +173 -0
- package/e2e/tier6-live-agent.test.mjs +759 -0
- package/e2e/vitest.config.e2e.mjs +1 -1
- package/hooks/hooks.json +15 -8
- package/package.json +13 -1
- package/references/agent-inbox/CLAUDE.md +151 -0
- package/references/agent-inbox/README.md +238 -0
- package/references/agent-inbox/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
- package/references/agent-inbox/docs/DESIGN.md +1156 -0
- package/references/agent-inbox/hooks/inbox-hook.mjs +119 -0
- package/references/agent-inbox/hooks/register-hook.mjs +69 -0
- package/references/agent-inbox/package-lock.json +3347 -0
- package/references/agent-inbox/package.json +58 -0
- package/references/agent-inbox/rules/agent-inbox.md +78 -0
- package/references/agent-inbox/src/federation/address.ts +61 -0
- package/references/agent-inbox/src/federation/connection-manager.ts +573 -0
- package/references/agent-inbox/src/federation/delivery-queue.ts +222 -0
- package/references/agent-inbox/src/federation/index.ts +6 -0
- package/references/agent-inbox/src/federation/routing-engine.ts +188 -0
- package/references/agent-inbox/src/federation/trust.ts +71 -0
- package/references/agent-inbox/src/index.ts +390 -0
- package/references/agent-inbox/src/ipc/ipc-server.ts +207 -0
- package/references/agent-inbox/src/jsonrpc/mail-server.ts +382 -0
- package/references/agent-inbox/src/map/map-client.ts +414 -0
- package/references/agent-inbox/src/mcp/mcp-server.ts +272 -0
- package/references/agent-inbox/src/mesh/delivery-bridge.ts +110 -0
- package/references/agent-inbox/src/mesh/mesh-connector.ts +41 -0
- package/references/agent-inbox/src/mesh/mesh-transport.ts +157 -0
- package/references/agent-inbox/src/mesh/type-mapper.ts +239 -0
- package/references/agent-inbox/src/push/notifier.ts +233 -0
- package/references/agent-inbox/src/registry/warm-registry.ts +255 -0
- package/references/agent-inbox/src/router/message-router.ts +175 -0
- package/references/agent-inbox/src/storage/interface.ts +48 -0
- package/references/agent-inbox/src/storage/memory.ts +145 -0
- package/references/agent-inbox/src/storage/sqlite.ts +671 -0
- package/references/agent-inbox/src/traceability/traceability.ts +183 -0
- package/references/agent-inbox/src/types.ts +303 -0
- package/references/agent-inbox/test/federation/address.test.ts +101 -0
- package/references/agent-inbox/test/federation/connection-manager.test.ts +546 -0
- package/references/agent-inbox/test/federation/delivery-queue.test.ts +159 -0
- package/references/agent-inbox/test/federation/integration.test.ts +857 -0
- package/references/agent-inbox/test/federation/routing-engine.test.ts +117 -0
- package/references/agent-inbox/test/federation/sdk-integration.test.ts +744 -0
- package/references/agent-inbox/test/federation/trust.test.ts +89 -0
- package/references/agent-inbox/test/ipc-jsonrpc.test.ts +113 -0
- package/references/agent-inbox/test/ipc-server.test.ts +197 -0
- package/references/agent-inbox/test/mail-server.test.ts +285 -0
- package/references/agent-inbox/test/map-client.test.ts +408 -0
- package/references/agent-inbox/test/mesh/delivery-bridge.test.ts +178 -0
- package/references/agent-inbox/test/mesh/e2e-mesh.test.ts +527 -0
- package/references/agent-inbox/test/mesh/e2e-real-meshpeer.test.ts +629 -0
- package/references/agent-inbox/test/mesh/federation-mesh.test.ts +269 -0
- package/references/agent-inbox/test/mesh/mesh-connector.test.ts +66 -0
- package/references/agent-inbox/test/mesh/mesh-transport.test.ts +191 -0
- package/references/agent-inbox/test/mesh/meshpeer-integration.test.ts +442 -0
- package/references/agent-inbox/test/mesh/mock-mesh.ts +125 -0
- package/references/agent-inbox/test/mesh/mock-meshpeer.ts +266 -0
- package/references/agent-inbox/test/mesh/type-mapper.test.ts +226 -0
- package/references/agent-inbox/test/message-router.test.ts +184 -0
- package/references/agent-inbox/test/push-notifier.test.ts +139 -0
- package/references/agent-inbox/test/registry/warm-registry.test.ts +171 -0
- package/references/agent-inbox/test/sqlite-prefix.test.ts +192 -0
- package/references/agent-inbox/test/sqlite-storage.test.ts +243 -0
- package/references/agent-inbox/test/storage.test.ts +196 -0
- package/references/agent-inbox/test/traceability.test.ts +123 -0
- package/references/agent-inbox/test/wake.test.ts +330 -0
- package/references/agent-inbox/tsconfig.json +20 -0
- package/references/agent-inbox/tsup.config.ts +10 -0
- package/references/agent-inbox/vitest.config.ts +8 -0
- package/references/minimem/.claude/settings.json +7 -0
- package/references/minimem/.sudocode/issues.jsonl +18 -0
- package/references/minimem/.sudocode/specs.jsonl +1 -0
- package/references/minimem/CLAUDE.md +329 -0
- package/references/minimem/README.md +565 -0
- package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
- package/references/minimem/claude-plugin/.mcp.json +7 -0
- package/references/minimem/claude-plugin/README.md +158 -0
- package/references/minimem/claude-plugin/commands/recall.md +47 -0
- package/references/minimem/claude-plugin/commands/remember.md +41 -0
- package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
- package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
- package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
- package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
- package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
- package/references/minimem/media/banner.png +0 -0
- package/references/minimem/package-lock.json +5373 -0
- package/references/minimem/package.json +76 -0
- package/references/minimem/scripts/postbuild.js +49 -0
- package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
- package/references/minimem/src/__tests__/errors.test.ts +265 -0
- package/references/minimem/src/__tests__/helpers.ts +199 -0
- package/references/minimem/src/__tests__/internal.test.ts +407 -0
- package/references/minimem/src/__tests__/knowledge-frontmatter.test.ts +148 -0
- package/references/minimem/src/__tests__/knowledge.test.ts +148 -0
- package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
- package/references/minimem/src/__tests__/session.test.ts +190 -0
- package/references/minimem/src/cli/__tests__/commands.test.ts +760 -0
- package/references/minimem/src/cli/__tests__/contained-layout.test.ts +286 -0
- package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
- package/references/minimem/src/cli/commands/append.ts +76 -0
- package/references/minimem/src/cli/commands/config.ts +262 -0
- package/references/minimem/src/cli/commands/conflicts.ts +415 -0
- package/references/minimem/src/cli/commands/daemon.ts +169 -0
- package/references/minimem/src/cli/commands/index.ts +12 -0
- package/references/minimem/src/cli/commands/init.ts +166 -0
- package/references/minimem/src/cli/commands/mcp.ts +221 -0
- package/references/minimem/src/cli/commands/push-pull.ts +213 -0
- package/references/minimem/src/cli/commands/search.ts +223 -0
- package/references/minimem/src/cli/commands/status.ts +84 -0
- package/references/minimem/src/cli/commands/store.ts +189 -0
- package/references/minimem/src/cli/commands/sync-init.ts +290 -0
- package/references/minimem/src/cli/commands/sync.ts +70 -0
- package/references/minimem/src/cli/commands/upsert.ts +197 -0
- package/references/minimem/src/cli/config.ts +611 -0
- package/references/minimem/src/cli/index.ts +299 -0
- package/references/minimem/src/cli/shared.ts +189 -0
- package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
- package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
- package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
- package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
- package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
- package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
- package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
- package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
- package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
- package/references/minimem/src/cli/sync/central.ts +292 -0
- package/references/minimem/src/cli/sync/conflicts.ts +205 -0
- package/references/minimem/src/cli/sync/daemon.ts +407 -0
- package/references/minimem/src/cli/sync/detection.ts +138 -0
- package/references/minimem/src/cli/sync/index.ts +107 -0
- package/references/minimem/src/cli/sync/operations.ts +373 -0
- package/references/minimem/src/cli/sync/registry.ts +279 -0
- package/references/minimem/src/cli/sync/state.ts +358 -0
- package/references/minimem/src/cli/sync/validation.ts +206 -0
- package/references/minimem/src/cli/sync/watcher.ts +237 -0
- package/references/minimem/src/cli/version.ts +34 -0
- package/references/minimem/src/core/index.ts +9 -0
- package/references/minimem/src/core/indexer.ts +628 -0
- package/references/minimem/src/core/searcher.ts +221 -0
- package/references/minimem/src/db/schema.ts +183 -0
- package/references/minimem/src/db/sqlite-vec.ts +24 -0
- package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
- package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
- package/references/minimem/src/embeddings/batch-openai.ts +409 -0
- package/references/minimem/src/embeddings/embeddings.ts +434 -0
- package/references/minimem/src/index.ts +132 -0
- package/references/minimem/src/internal.ts +299 -0
- package/references/minimem/src/minimem.ts +1291 -0
- package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
- package/references/minimem/src/search/graph.ts +234 -0
- package/references/minimem/src/search/hybrid.ts +151 -0
- package/references/minimem/src/search/search.ts +256 -0
- package/references/minimem/src/server/__tests__/mcp.test.ts +347 -0
- package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
- package/references/minimem/src/server/mcp.ts +326 -0
- package/references/minimem/src/server/tools.ts +720 -0
- package/references/minimem/src/session.ts +460 -0
- package/references/minimem/src/store/__tests__/manifest.test.ts +177 -0
- package/references/minimem/src/store/__tests__/materialize.test.ts +52 -0
- package/references/minimem/src/store/__tests__/store-graph.test.ts +228 -0
- package/references/minimem/src/store/index.ts +27 -0
- package/references/minimem/src/store/manifest.ts +203 -0
- package/references/minimem/src/store/materialize.ts +185 -0
- package/references/minimem/src/store/store-graph.ts +252 -0
- package/references/minimem/tsconfig.json +19 -0
- package/references/minimem/tsup.config.ts +26 -0
- package/references/minimem/vitest.config.ts +29 -0
- package/references/openteams/src/cli/generate.ts +23 -1
- package/references/openteams/src/generators/agent-prompt-generator.test.ts +94 -0
- package/references/openteams/src/generators/agent-prompt-generator.ts +42 -13
- package/references/openteams/src/generators/package-generator.ts +9 -1
- package/references/openteams/src/generators/skill-generator.test.ts +28 -0
- package/references/openteams/src/generators/skill-generator.ts +10 -4
- package/references/skill-tree/.claude/settings.json +6 -0
- package/references/skill-tree/.sudocode/issues.jsonl +19 -0
- package/references/skill-tree/.sudocode/specs.jsonl +3 -0
- package/references/skill-tree/CLAUDE.md +132 -0
- package/references/skill-tree/README.md +396 -0
- package/references/skill-tree/docs/GAPS_v1.md +221 -0
- package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
- package/references/skill-tree/docs/TODOS.md +91 -0
- package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
- package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
- package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
- package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
- package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
- package/references/skill-tree/docs/scraper/README.md +170 -0
- package/references/skill-tree/examples/basic-usage.ts +157 -0
- package/references/skill-tree/package-lock.json +1852 -0
- package/references/skill-tree/package.json +66 -0
- package/references/skill-tree/plan.md +78 -0
- package/references/skill-tree/scraper/README.md +123 -0
- package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
- package/references/skill-tree/scraper/docs/PLAN.md +336 -0
- package/references/skill-tree/scraper/drizzle.config.ts +10 -0
- package/references/skill-tree/scraper/package-lock.json +6329 -0
- package/references/skill-tree/scraper/package.json +68 -0
- package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
- package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
- package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
- package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
- package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
- package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
- package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
- package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
- package/references/skill-tree/scraper/tsup.config.ts +14 -0
- package/references/skill-tree/scraper/vitest.config.ts +17 -0
- package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
- package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
- package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
- package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
- package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
- package/references/skill-tree/test/run-all.ts +106 -0
- package/references/skill-tree/test/utils.ts +128 -0
- package/references/skill-tree/vitest.config.ts +16 -0
- package/references/swarmkit/src/commands/init/phases/configure.ts +0 -22
- package/references/swarmkit/src/commands/init/phases/global-setup.ts +5 -3
- package/references/swarmkit/src/commands/init/wizard.ts +2 -2
- package/references/swarmkit/src/packages/setup.test.ts +53 -7
- package/references/swarmkit/src/packages/setup.ts +37 -1
- package/scripts/bootstrap.mjs +26 -1
- package/scripts/generate-agents.mjs +5 -1
- package/scripts/map-hook.mjs +97 -64
- package/scripts/map-sidecar.mjs +179 -25
- package/scripts/team-loader.mjs +12 -41
- package/skills/swarm/SKILL.md +89 -25
- package/src/__tests__/agent-generator.test.mjs +6 -13
- package/src/__tests__/bootstrap.test.mjs +124 -1
- package/src/__tests__/config.test.mjs +200 -27
- package/src/__tests__/e2e-live-map.test.mjs +536 -0
- package/src/__tests__/e2e-mesh-sidecar.test.mjs +570 -0
- package/src/__tests__/e2e-native-task-hooks.test.mjs +376 -0
- package/src/__tests__/e2e-sidecar-bridge.test.mjs +477 -0
- package/src/__tests__/helpers.mjs +13 -0
- package/src/__tests__/inbox.test.mjs +22 -89
- package/src/__tests__/index.test.mjs +35 -9
- package/src/__tests__/integration.test.mjs +513 -0
- package/src/__tests__/map-events.test.mjs +514 -150
- package/src/__tests__/mesh-connection.test.mjs +308 -0
- package/src/__tests__/opentasks-client.test.mjs +517 -0
- package/src/__tests__/paths.test.mjs +185 -41
- package/src/__tests__/sidecar-client.test.mjs +35 -0
- package/src/__tests__/sidecar-server.test.mjs +124 -0
- package/src/__tests__/skilltree-client.test.mjs +80 -0
- package/src/agent-generator.mjs +104 -33
- package/src/bootstrap.mjs +150 -10
- package/src/config.mjs +81 -17
- package/src/context-output.mjs +58 -8
- package/src/inbox.mjs +9 -54
- package/src/index.mjs +39 -8
- package/src/map-connection.mjs +4 -3
- package/src/map-events.mjs +350 -80
- package/src/mesh-connection.mjs +148 -0
- package/src/opentasks-client.mjs +269 -0
- package/src/paths.mjs +182 -27
- package/src/sessionlog.mjs +14 -9
- package/src/sidecar-client.mjs +81 -27
- package/src/sidecar-server.mjs +175 -16
- package/src/skilltree-client.mjs +173 -0
- package/src/template.mjs +68 -4
- 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
|
+
});
|