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,857 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { InMemoryStorage } from "../../src/storage/memory.js";
|
|
4
|
+
import { MessageRouter } from "../../src/router/message-router.js";
|
|
5
|
+
import { ConnectionManager } from "../../src/federation/connection-manager.js";
|
|
6
|
+
import { WarmRegistry } from "../../src/registry/warm-registry.js";
|
|
7
|
+
import {
|
|
8
|
+
parseAddress,
|
|
9
|
+
formatAddress,
|
|
10
|
+
isRemoteAddress,
|
|
11
|
+
isBroadcastAddress,
|
|
12
|
+
} from "../../src/federation/address.js";
|
|
13
|
+
import type { Agent, Message } from "../../src/types.js";
|
|
14
|
+
import type {
|
|
15
|
+
MapConnection,
|
|
16
|
+
MapAgentConnectionClass,
|
|
17
|
+
IncomingMapMessage,
|
|
18
|
+
} from "../../src/map/map-client.js";
|
|
19
|
+
|
|
20
|
+
function makeAgent(id: string, scope = "default"): Agent {
|
|
21
|
+
const now = new Date().toISOString();
|
|
22
|
+
return {
|
|
23
|
+
agent_id: id,
|
|
24
|
+
display_name: id,
|
|
25
|
+
scope,
|
|
26
|
+
status: "active",
|
|
27
|
+
metadata: {},
|
|
28
|
+
registered_at: now,
|
|
29
|
+
last_active_at: now,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Integration tests — Step 19 from PLAN.md.
|
|
35
|
+
*
|
|
36
|
+
* These tests exercise the full flow across multiple components:
|
|
37
|
+
* MessageRouter + ConnectionManager + RoutingEngine + DeliveryQueue +
|
|
38
|
+
* TrustManager + WarmRegistry + address parsing.
|
|
39
|
+
*/
|
|
40
|
+
describe("Federation Integration", () => {
|
|
41
|
+
let storage: InMemoryStorage;
|
|
42
|
+
let events: EventEmitter;
|
|
43
|
+
let router: MessageRouter;
|
|
44
|
+
let federation: ConnectionManager;
|
|
45
|
+
let registry: WarmRegistry;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.useFakeTimers();
|
|
49
|
+
storage = new InMemoryStorage();
|
|
50
|
+
events = new EventEmitter();
|
|
51
|
+
router = new MessageRouter(storage, events, "default");
|
|
52
|
+
|
|
53
|
+
federation = new ConnectionManager(events, {
|
|
54
|
+
systemId: "system-1",
|
|
55
|
+
trust: {
|
|
56
|
+
allowedServers: ["system-2", "hub-system"],
|
|
57
|
+
scopePermissions: {},
|
|
58
|
+
requireAuth: false,
|
|
59
|
+
},
|
|
60
|
+
routing: { strategy: "table" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
registry = new WarmRegistry(storage, events, {
|
|
64
|
+
gracePeriodMs: 5000,
|
|
65
|
+
retainExpiredMs: 30000,
|
|
66
|
+
requeueOnReconnect: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.setFederation(federation);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await federation.destroy();
|
|
74
|
+
registry.destroy();
|
|
75
|
+
vi.useRealTimers();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("1. Federated send — local agent to remote agent", () => {
|
|
79
|
+
it("should route a message from local agent to remote agent via federation", async () => {
|
|
80
|
+
// Register local sender
|
|
81
|
+
storage.putAgent(makeAgent("agent-a"));
|
|
82
|
+
|
|
83
|
+
// Establish federation with system-2 and expose remote agent
|
|
84
|
+
await federation.federate({
|
|
85
|
+
systemId: "system-2",
|
|
86
|
+
url: "ws://system-2:3001",
|
|
87
|
+
});
|
|
88
|
+
federation.routing.updateFromExposure("system-2", [
|
|
89
|
+
{ agentId: "agent-c" },
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
// Track federation routing event
|
|
93
|
+
const routeSpy = vi.fn();
|
|
94
|
+
events.on("federation.route", routeSpy);
|
|
95
|
+
|
|
96
|
+
// Send message from agent-a to agent-c@system-2
|
|
97
|
+
const msg = await router.routeMessage({
|
|
98
|
+
from: "agent-a",
|
|
99
|
+
to: "agent-c@system-2",
|
|
100
|
+
payload: "Hello from system-1!",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(msg.id).toBeDefined();
|
|
104
|
+
expect(msg.sender_id).toBe("agent-a");
|
|
105
|
+
expect(msg.recipients[0].agent_id).toBe("agent-c@system-2");
|
|
106
|
+
// Federation route completes synchronously, so delivered_at is set
|
|
107
|
+
expect(msg.recipients[0].delivered_at).toBeDefined();
|
|
108
|
+
|
|
109
|
+
// The federation route event should have fired
|
|
110
|
+
expect(routeSpy).toHaveBeenCalledWith(
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
peerId: "system-2",
|
|
113
|
+
message: expect.objectContaining({ id: msg.id }),
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should store the message locally even when routing remotely", async () => {
|
|
119
|
+
storage.putAgent(makeAgent("agent-a"));
|
|
120
|
+
await federation.federate({
|
|
121
|
+
systemId: "system-2",
|
|
122
|
+
url: "ws://system-2:3001",
|
|
123
|
+
});
|
|
124
|
+
federation.routing.updateFromExposure("system-2", [
|
|
125
|
+
{ agentId: "agent-c" },
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const msg = await router.routeMessage({
|
|
129
|
+
from: "agent-a",
|
|
130
|
+
to: "agent-c@system-2",
|
|
131
|
+
payload: "Stored locally",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const stored = storage.getMessage(msg.id);
|
|
135
|
+
expect(stored).toBeDefined();
|
|
136
|
+
expect(stored!.content).toEqual({ type: "text", text: "Stored locally" });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("2. Hub relay — hierarchical routing", () => {
|
|
141
|
+
it("should delegate to upstream hub for agent-only address not in routing table", async () => {
|
|
142
|
+
const hubFederation = new ConnectionManager(events, {
|
|
143
|
+
systemId: "system-1",
|
|
144
|
+
trust: {
|
|
145
|
+
allowedServers: ["hub-system"],
|
|
146
|
+
scopePermissions: {},
|
|
147
|
+
requireAuth: false,
|
|
148
|
+
},
|
|
149
|
+
routing: {
|
|
150
|
+
strategy: "hierarchical",
|
|
151
|
+
upstream: ["hub-system"],
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await hubFederation.federate({
|
|
156
|
+
systemId: "hub-system",
|
|
157
|
+
url: "ws://hub:3000",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const routeSpy = vi.fn();
|
|
161
|
+
events.on("federation.route", routeSpy);
|
|
162
|
+
|
|
163
|
+
// Use agent-only address (no @system) that's not in routing table.
|
|
164
|
+
// resolveRoute returns null → handleUnknownRoute → hierarchical → upstream hub
|
|
165
|
+
const msg: Message = {
|
|
166
|
+
id: "msg-hub-1",
|
|
167
|
+
scope: "default",
|
|
168
|
+
sender_id: "agent-a",
|
|
169
|
+
recipients: [{ agent_id: "unknown-agent@hub-system", kind: "to" }],
|
|
170
|
+
content: { type: "text", text: "relay me" },
|
|
171
|
+
importance: "normal",
|
|
172
|
+
metadata: {},
|
|
173
|
+
created_at: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = await hubFederation.route(msg);
|
|
177
|
+
// Address has @hub-system so resolveRoute returns "hub-system" directly,
|
|
178
|
+
// and since hub-system is connected, it delivers
|
|
179
|
+
expect(result.delivered).toBe(true);
|
|
180
|
+
expect(result.peerId).toBe("hub-system");
|
|
181
|
+
expect(routeSpy).toHaveBeenCalledWith(
|
|
182
|
+
expect.objectContaining({ peerId: "hub-system" })
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await hubFederation.destroy();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should relay via upstream hub when system-qualified address has no direct peer link", async () => {
|
|
189
|
+
const hubFederation = new ConnectionManager(events, {
|
|
190
|
+
systemId: "system-1",
|
|
191
|
+
trust: {
|
|
192
|
+
allowedServers: ["hub-system"],
|
|
193
|
+
scopePermissions: {},
|
|
194
|
+
requireAuth: false,
|
|
195
|
+
},
|
|
196
|
+
routing: {
|
|
197
|
+
strategy: "hierarchical",
|
|
198
|
+
upstream: ["hub-system"],
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await hubFederation.federate({
|
|
203
|
+
systemId: "hub-system",
|
|
204
|
+
url: "ws://hub:3000",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Route to far-system which has no direct peer link.
|
|
208
|
+
// With hierarchical strategy, falls through to upstream hub relay.
|
|
209
|
+
const msg: Message = {
|
|
210
|
+
id: "msg-hub-2",
|
|
211
|
+
scope: "default",
|
|
212
|
+
sender_id: "agent-a",
|
|
213
|
+
recipients: [{ agent_id: "agent-x@far-system", kind: "to" }],
|
|
214
|
+
content: { type: "text", text: "no direct link" },
|
|
215
|
+
importance: "normal",
|
|
216
|
+
metadata: {},
|
|
217
|
+
created_at: new Date().toISOString(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = await hubFederation.route(msg);
|
|
221
|
+
// Hierarchical strategy delegates to upstream hub instead of queuing
|
|
222
|
+
expect(result.delivered).toBe(true);
|
|
223
|
+
expect(result.peerId).toBe("hub-system");
|
|
224
|
+
|
|
225
|
+
await hubFederation.destroy();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should queue when system-qualified address has no peer link (table strategy)", async () => {
|
|
229
|
+
const tableFederation = new ConnectionManager(events, {
|
|
230
|
+
systemId: "system-1",
|
|
231
|
+
trust: {
|
|
232
|
+
allowedServers: [],
|
|
233
|
+
scopePermissions: {},
|
|
234
|
+
requireAuth: false,
|
|
235
|
+
},
|
|
236
|
+
routing: {
|
|
237
|
+
strategy: "table",
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// No peers connected — route to far-system queues immediately
|
|
242
|
+
const msg: Message = {
|
|
243
|
+
id: "msg-table-1",
|
|
244
|
+
scope: "default",
|
|
245
|
+
sender_id: "agent-a",
|
|
246
|
+
recipients: [{ agent_id: "agent-x@far-system", kind: "to" }],
|
|
247
|
+
content: { type: "text", text: "no direct link" },
|
|
248
|
+
importance: "normal",
|
|
249
|
+
metadata: {},
|
|
250
|
+
created_at: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const result = await tableFederation.route(msg);
|
|
254
|
+
expect(result.delivered).toBe(false);
|
|
255
|
+
expect(result.queued).toBe(true);
|
|
256
|
+
expect(result.peerId).toBe("far-system");
|
|
257
|
+
|
|
258
|
+
await tableFederation.destroy();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("3. Warm registry — disconnect, queue, reconnect, flush", () => {
|
|
263
|
+
it("should queue messages for away agent and flush on reconnect", async () => {
|
|
264
|
+
// Register agent-b via warm registry
|
|
265
|
+
registry.register(makeAgent("agent-b"));
|
|
266
|
+
expect(registry.getStatus("agent-b")).toBe("active");
|
|
267
|
+
|
|
268
|
+
// Agent disconnects — goes to "away"
|
|
269
|
+
registry.disconnect("agent-b");
|
|
270
|
+
expect(registry.getStatus("agent-b")).toBe("away");
|
|
271
|
+
expect(registry.isRoutable("agent-b")).toBe(true);
|
|
272
|
+
|
|
273
|
+
// Agent reconnects before grace period
|
|
274
|
+
registry.reconnect("agent-b");
|
|
275
|
+
expect(registry.getStatus("agent-b")).toBe("active");
|
|
276
|
+
|
|
277
|
+
// Verify it didn't expire
|
|
278
|
+
vi.advanceTimersByTime(6000);
|
|
279
|
+
expect(registry.getStatus("agent-b")).toBe("active");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should expire agent after grace period", () => {
|
|
283
|
+
registry.register(makeAgent("agent-b"));
|
|
284
|
+
registry.disconnect("agent-b");
|
|
285
|
+
|
|
286
|
+
vi.advanceTimersByTime(5001);
|
|
287
|
+
expect(registry.getStatus("agent-b")).toBe("expired");
|
|
288
|
+
expect(registry.isRoutable("agent-b")).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("4. Offline peer — messages queue and flush on reconnect", () => {
|
|
293
|
+
it("should queue messages when peer goes offline and flush on reconnect", async () => {
|
|
294
|
+
// Federate then disconnect peer
|
|
295
|
+
await federation.federate({
|
|
296
|
+
systemId: "system-2",
|
|
297
|
+
url: "ws://system-2:3001",
|
|
298
|
+
});
|
|
299
|
+
federation.routing.updateFromExposure("system-2", [
|
|
300
|
+
{ agentId: "agent-c" },
|
|
301
|
+
]);
|
|
302
|
+
await federation.disconnect("system-2");
|
|
303
|
+
|
|
304
|
+
// Send message to offline peer — should be queued
|
|
305
|
+
const msg: Message = {
|
|
306
|
+
id: "msg-q-1",
|
|
307
|
+
scope: "default",
|
|
308
|
+
sender_id: "agent-a",
|
|
309
|
+
recipients: [{ agent_id: "agent-c@system-2", kind: "to" }],
|
|
310
|
+
content: { type: "text", text: "you're offline" },
|
|
311
|
+
importance: "normal",
|
|
312
|
+
metadata: {},
|
|
313
|
+
created_at: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const result = await federation.route(msg);
|
|
317
|
+
expect(result.delivered).toBe(false);
|
|
318
|
+
expect(result.queued).toBe(true);
|
|
319
|
+
expect(federation.queue.size("system-2")).toBe(1);
|
|
320
|
+
|
|
321
|
+
// Peer reconnects — queue should flush
|
|
322
|
+
const routeSpy = vi.fn();
|
|
323
|
+
events.on("federation.route", routeSpy);
|
|
324
|
+
events.on("federation.flushing", (data) => {
|
|
325
|
+
expect(data.count).toBe(1);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await federation.federate({
|
|
329
|
+
systemId: "system-2",
|
|
330
|
+
url: "ws://system-2:3001",
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Queue should be drained
|
|
334
|
+
expect(federation.queue.size("system-2")).toBe(0);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("5. Trust enforcement — unknown system rejected", () => {
|
|
339
|
+
it("should reject federation from unknown system", async () => {
|
|
340
|
+
await expect(
|
|
341
|
+
federation.federate({
|
|
342
|
+
systemId: "evil-system",
|
|
343
|
+
url: "ws://evil:6666",
|
|
344
|
+
})
|
|
345
|
+
).rejects.toThrow("Federation denied");
|
|
346
|
+
|
|
347
|
+
expect(federation.isConnected("evil-system")).toBe(false);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should allow federation from approved systems", async () => {
|
|
351
|
+
const link = await federation.federate({
|
|
352
|
+
systemId: "system-2",
|
|
353
|
+
url: "ws://system-2:3001",
|
|
354
|
+
});
|
|
355
|
+
expect(link.status).toBe("connected");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should deny routing when scope trust fails", async () => {
|
|
359
|
+
const restrictedFederation = new ConnectionManager(events, {
|
|
360
|
+
systemId: "system-1",
|
|
361
|
+
trust: {
|
|
362
|
+
allowedServers: ["system-2"],
|
|
363
|
+
scopePermissions: { "system-2": ["allowed-scope"] },
|
|
364
|
+
requireAuth: false,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await restrictedFederation.federate({
|
|
369
|
+
systemId: "system-2",
|
|
370
|
+
url: "ws://system-2:3001",
|
|
371
|
+
});
|
|
372
|
+
restrictedFederation.routing.updateFromExposure("system-2", [
|
|
373
|
+
{ agentId: "agent-x" },
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
// Scope trust is currently a stub (always passes), so this verifies
|
|
377
|
+
// the trust check is called in the route path
|
|
378
|
+
const msg: Message = {
|
|
379
|
+
id: "msg-trust-1",
|
|
380
|
+
scope: "default",
|
|
381
|
+
sender_id: "agent-a",
|
|
382
|
+
recipients: [{ agent_id: "agent-x@system-2", kind: "to" }],
|
|
383
|
+
content: { type: "text", text: "test" },
|
|
384
|
+
importance: "normal",
|
|
385
|
+
metadata: {},
|
|
386
|
+
created_at: new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const result = await restrictedFederation.route(msg);
|
|
390
|
+
// Stub always allows, so delivery succeeds
|
|
391
|
+
expect(result.delivered).toBe(true);
|
|
392
|
+
|
|
393
|
+
await restrictedFederation.destroy();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe("6. Routing strategies", () => {
|
|
398
|
+
// NOTE: In the current implementation, route() only processes recipients
|
|
399
|
+
// where isRemoteAddress() is true (i.e., address contains @system).
|
|
400
|
+
// resolveRoute() returns addr.system directly for system-qualified addresses.
|
|
401
|
+
// This means handleUnknownRoute (broadcast/hierarchical) is only reachable
|
|
402
|
+
// via direct calls or future changes to support agent-only federation routing.
|
|
403
|
+
// These tests verify the actual routing behavior through ConnectionManager.
|
|
404
|
+
|
|
405
|
+
it("table strategy: system-qualified address delivers to connected peer", async () => {
|
|
406
|
+
await federation.federate({
|
|
407
|
+
systemId: "system-2",
|
|
408
|
+
url: "ws://system-2:3001",
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const routeSpy = vi.fn();
|
|
412
|
+
events.on("federation.route", routeSpy);
|
|
413
|
+
|
|
414
|
+
const msg: Message = {
|
|
415
|
+
id: "msg-direct-1",
|
|
416
|
+
scope: "default",
|
|
417
|
+
sender_id: "agent-a",
|
|
418
|
+
recipients: [{ agent_id: "unknown-agent@system-2", kind: "to" }],
|
|
419
|
+
content: { type: "text", text: "direct to system" },
|
|
420
|
+
importance: "normal",
|
|
421
|
+
metadata: {},
|
|
422
|
+
created_at: new Date().toISOString(),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const result = await federation.route(msg);
|
|
426
|
+
expect(result.delivered).toBe(true);
|
|
427
|
+
expect(result.peerId).toBe("system-2");
|
|
428
|
+
expect(routeSpy).toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("table strategy: system-qualified address queues when peer offline", async () => {
|
|
432
|
+
await federation.federate({
|
|
433
|
+
systemId: "system-2",
|
|
434
|
+
url: "ws://system-2:3001",
|
|
435
|
+
});
|
|
436
|
+
await federation.disconnect("system-2");
|
|
437
|
+
|
|
438
|
+
const msg: Message = {
|
|
439
|
+
id: "msg-miss-1",
|
|
440
|
+
scope: "default",
|
|
441
|
+
sender_id: "agent-a",
|
|
442
|
+
recipients: [{ agent_id: "agent-c@system-2", kind: "to" }],
|
|
443
|
+
content: { type: "text", text: "peer offline" },
|
|
444
|
+
importance: "normal",
|
|
445
|
+
metadata: {},
|
|
446
|
+
created_at: new Date().toISOString(),
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const result = await federation.route(msg);
|
|
450
|
+
expect(result.delivered).toBe(false);
|
|
451
|
+
expect(result.queued).toBe(true);
|
|
452
|
+
expect(result.peerId).toBe("system-2");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("agent-only addresses are not treated as remote", async () => {
|
|
456
|
+
const msg: Message = {
|
|
457
|
+
id: "msg-local-1",
|
|
458
|
+
scope: "default",
|
|
459
|
+
sender_id: "agent-a",
|
|
460
|
+
recipients: [{ agent_id: "local-agent", kind: "to" }],
|
|
461
|
+
content: { type: "text", text: "local only" },
|
|
462
|
+
importance: "normal",
|
|
463
|
+
metadata: {},
|
|
464
|
+
created_at: new Date().toISOString(),
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const result = await federation.route(msg);
|
|
468
|
+
expect(result.delivered).toBe(false);
|
|
469
|
+
expect(result.error).toContain("No remote recipients");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("routing engine resolveRoute uses table for agent-only lookups", () => {
|
|
473
|
+
federation.routing.updateFromExposure("system-2", [
|
|
474
|
+
{ agentId: "known-agent" },
|
|
475
|
+
]);
|
|
476
|
+
expect(federation.routing.resolveRoute({ agent: "known-agent" })).toBe(
|
|
477
|
+
"system-2"
|
|
478
|
+
);
|
|
479
|
+
expect(
|
|
480
|
+
federation.routing.resolveRoute({ agent: "unknown-agent" })
|
|
481
|
+
).toBeNull();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("routing engine strategy accessors work correctly", () => {
|
|
485
|
+
expect(federation.routing.getStrategy()).toBe("table");
|
|
486
|
+
|
|
487
|
+
const broadcastCm = new ConnectionManager(events, {
|
|
488
|
+
systemId: "s",
|
|
489
|
+
routing: { strategy: "broadcast", broadcastTimeout: 3000 },
|
|
490
|
+
});
|
|
491
|
+
expect(broadcastCm.routing.getStrategy()).toBe("broadcast");
|
|
492
|
+
expect(broadcastCm.routing.getBroadcastTimeout()).toBe(3000);
|
|
493
|
+
broadcastCm.destroy();
|
|
494
|
+
|
|
495
|
+
const hierCm = new ConnectionManager(events, {
|
|
496
|
+
systemId: "s",
|
|
497
|
+
routing: { strategy: "hierarchical", upstream: ["hub-1"] },
|
|
498
|
+
});
|
|
499
|
+
expect(hierCm.routing.getStrategy()).toBe("hierarchical");
|
|
500
|
+
expect(hierCm.routing.getUpstream()).toEqual(["hub-1"]);
|
|
501
|
+
hierCm.destroy();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("routing table entries expire based on TTL", () => {
|
|
505
|
+
const shortTtlCm = new ConnectionManager(events, {
|
|
506
|
+
systemId: "s",
|
|
507
|
+
routing: { strategy: "table", tableTTL: 100 },
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
shortTtlCm.routing.updateFromExposure("peer-1", [
|
|
511
|
+
{ agentId: "temp-agent" },
|
|
512
|
+
]);
|
|
513
|
+
expect(shortTtlCm.routing.lookupAgent("temp-agent")).toBe("peer-1");
|
|
514
|
+
|
|
515
|
+
// Advance past TTL
|
|
516
|
+
vi.advanceTimersByTime(150);
|
|
517
|
+
expect(shortTtlCm.routing.lookupAgent("temp-agent")).toBeNull();
|
|
518
|
+
|
|
519
|
+
shortTtlCm.destroy();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
describe("7. Address parsing — all formats resolve correctly", () => {
|
|
524
|
+
it("local address", () => {
|
|
525
|
+
const addr = parseAddress("agent-beta");
|
|
526
|
+
expect(addr).toEqual({ agent: "agent-beta" });
|
|
527
|
+
expect(isRemoteAddress(addr)).toBe(false);
|
|
528
|
+
expect(isBroadcastAddress(addr)).toBe(false);
|
|
529
|
+
expect(formatAddress(addr)).toBe("agent-beta");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("federated address", () => {
|
|
533
|
+
const addr = parseAddress("agent-beta@backend-team");
|
|
534
|
+
expect(addr).toEqual({ agent: "agent-beta", system: "backend-team" });
|
|
535
|
+
expect(isRemoteAddress(addr)).toBe(true);
|
|
536
|
+
expect(isBroadcastAddress(addr)).toBe(false);
|
|
537
|
+
expect(formatAddress(addr)).toBe("agent-beta@backend-team");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("broadcast to system", () => {
|
|
541
|
+
const addr = parseAddress("@backend-team");
|
|
542
|
+
expect(addr).toEqual({ system: "backend-team" });
|
|
543
|
+
expect(isRemoteAddress(addr)).toBe(true);
|
|
544
|
+
expect(isBroadcastAddress(addr)).toBe(true);
|
|
545
|
+
expect(formatAddress(addr)).toBe("@backend-team");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("scoped federated address", () => {
|
|
549
|
+
const addr = parseAddress("agent-beta@backend-team/engineering");
|
|
550
|
+
expect(addr).toEqual({
|
|
551
|
+
agent: "agent-beta",
|
|
552
|
+
system: "backend-team",
|
|
553
|
+
scope: "engineering",
|
|
554
|
+
});
|
|
555
|
+
expect(isRemoteAddress(addr)).toBe(true);
|
|
556
|
+
expect(formatAddress(addr)).toBe("agent-beta@backend-team/engineering");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("round-trip all formats", () => {
|
|
560
|
+
const addresses = [
|
|
561
|
+
"agent-1",
|
|
562
|
+
"agent-1@system-2",
|
|
563
|
+
"@system-2",
|
|
564
|
+
"agent-1@system-2/scope-a",
|
|
565
|
+
];
|
|
566
|
+
for (const original of addresses) {
|
|
567
|
+
expect(formatAddress(parseAddress(original))).toBe(original);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe("8. System ID resolution", () => {
|
|
573
|
+
it("explicit config takes highest precedence", () => {
|
|
574
|
+
const cm = new ConnectionManager(events, { systemId: "explicit-id" });
|
|
575
|
+
expect(cm.getSystemId()).toEqual({ id: "explicit-id", source: "config" });
|
|
576
|
+
cm.destroy();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("MAP systemInfo is tier 2 (overrides auto, not config)", () => {
|
|
580
|
+
const cm = new ConnectionManager(events, {}); // auto-generated
|
|
581
|
+
expect(cm.getSystemId().source).toBe("auto");
|
|
582
|
+
|
|
583
|
+
cm.updateSystemIdFromMap("map-derived");
|
|
584
|
+
expect(cm.getSystemId()).toEqual({ id: "map-derived", source: "map" });
|
|
585
|
+
cm.destroy();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("explicit config cannot be overridden by MAP", () => {
|
|
589
|
+
const cm = new ConnectionManager(events, { systemId: "explicit-id" });
|
|
590
|
+
cm.updateSystemIdFromMap("map-derived");
|
|
591
|
+
expect(cm.getSystemId()).toEqual({ id: "explicit-id", source: "config" });
|
|
592
|
+
cm.destroy();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("auto-generated ID is stable format", () => {
|
|
596
|
+
const cm = new ConnectionManager(events, {});
|
|
597
|
+
const sid = cm.getSystemId();
|
|
598
|
+
expect(sid.source).toBe("auto");
|
|
599
|
+
expect(sid.id).toMatch(/^inbox-[0-9a-f]{8}$/);
|
|
600
|
+
cm.destroy();
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
describe("End-to-end: full message lifecycle with federation", () => {
|
|
605
|
+
it("should handle send → store → route → queue → flush cycle", async () => {
|
|
606
|
+
// Setup: local agent + federated peer
|
|
607
|
+
storage.putAgent(makeAgent("agent-a"));
|
|
608
|
+
registry.register(makeAgent("agent-a"));
|
|
609
|
+
|
|
610
|
+
await federation.federate({
|
|
611
|
+
systemId: "system-2",
|
|
612
|
+
url: "ws://system-2:3001",
|
|
613
|
+
});
|
|
614
|
+
federation.routing.updateFromExposure("system-2", [
|
|
615
|
+
{ agentId: "agent-c" },
|
|
616
|
+
]);
|
|
617
|
+
|
|
618
|
+
// Step 1: Send a message — should route via federation
|
|
619
|
+
const msg1 = await router.routeMessage({
|
|
620
|
+
from: "agent-a",
|
|
621
|
+
to: "agent-c@system-2",
|
|
622
|
+
payload: "Hello agent-c!",
|
|
623
|
+
subject: "Greeting",
|
|
624
|
+
});
|
|
625
|
+
expect(storage.getMessage(msg1.id)).toBeDefined();
|
|
626
|
+
|
|
627
|
+
// Step 2: Peer goes offline — next message gets queued
|
|
628
|
+
await federation.disconnect("system-2");
|
|
629
|
+
|
|
630
|
+
// Direct route (not via router, to test queue)
|
|
631
|
+
const msg2: Message = {
|
|
632
|
+
id: "msg-e2e-2",
|
|
633
|
+
scope: "default",
|
|
634
|
+
sender_id: "agent-a",
|
|
635
|
+
recipients: [{ agent_id: "agent-c@system-2", kind: "to" }],
|
|
636
|
+
content: { type: "text", text: "Are you there?" },
|
|
637
|
+
importance: "normal",
|
|
638
|
+
metadata: {},
|
|
639
|
+
created_at: new Date().toISOString(),
|
|
640
|
+
};
|
|
641
|
+
const result2 = await federation.route(msg2);
|
|
642
|
+
expect(result2.queued).toBe(true);
|
|
643
|
+
expect(federation.queue.size("system-2")).toBe(1);
|
|
644
|
+
|
|
645
|
+
// Step 3: Peer comes back — queue flushes
|
|
646
|
+
const flushSpy = vi.fn();
|
|
647
|
+
events.on("federation.flushing", flushSpy);
|
|
648
|
+
|
|
649
|
+
await federation.federate({
|
|
650
|
+
systemId: "system-2",
|
|
651
|
+
url: "ws://system-2:3001",
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
expect(flushSpy).toHaveBeenCalledWith(
|
|
655
|
+
expect.objectContaining({ peerId: "system-2", count: 1 })
|
|
656
|
+
);
|
|
657
|
+
expect(federation.queue.size("system-2")).toBe(0);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("should handle incoming federation message and store with correct recipient", async () => {
|
|
661
|
+
// Setup: local agent that should receive the message
|
|
662
|
+
storage.putAgent(makeAgent("agent-b"));
|
|
663
|
+
|
|
664
|
+
// Create federation with transport mock that captures onMessage handlers
|
|
665
|
+
const peerMessageHandlers: Array<(msg: IncomingMapMessage) => void> = [];
|
|
666
|
+
const sentMessages: Array<{
|
|
667
|
+
to: { agentId?: string; scope?: string };
|
|
668
|
+
payload: unknown;
|
|
669
|
+
meta?: Record<string, unknown>;
|
|
670
|
+
}> = [];
|
|
671
|
+
|
|
672
|
+
const mockSdkClass: MapAgentConnectionClass = {
|
|
673
|
+
connect: async () => ({
|
|
674
|
+
send: async (to, payload, meta) => {
|
|
675
|
+
sentMessages.push({ to, payload, meta });
|
|
676
|
+
},
|
|
677
|
+
onMessage: (handler: (msg: IncomingMapMessage) => void) => {
|
|
678
|
+
peerMessageHandlers.push(handler);
|
|
679
|
+
},
|
|
680
|
+
disconnect: async () => {},
|
|
681
|
+
}),
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// Create federation with real transport + incoming message handler wired to router
|
|
685
|
+
const transportFederation = new ConnectionManager(
|
|
686
|
+
events,
|
|
687
|
+
{
|
|
688
|
+
systemId: "system-1",
|
|
689
|
+
trust: {
|
|
690
|
+
allowedServers: ["system-2"],
|
|
691
|
+
scopePermissions: {},
|
|
692
|
+
requireAuth: false,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
sdkClass: mockSdkClass,
|
|
697
|
+
onIncomingMessage: ({ from, peerId, payload, meta }) => {
|
|
698
|
+
const targetAgent = (meta?.targetAgent as string) ?? from;
|
|
699
|
+
router.routeMessage({
|
|
700
|
+
from: `${from}@${peerId}`,
|
|
701
|
+
to: targetAgent,
|
|
702
|
+
payload,
|
|
703
|
+
subject: meta?.subject as string | undefined,
|
|
704
|
+
importance: meta?.importance as "high" | "normal" | undefined,
|
|
705
|
+
metadata: meta,
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
router.setFederation(transportFederation);
|
|
711
|
+
|
|
712
|
+
await transportFederation.federate({
|
|
713
|
+
systemId: "system-2",
|
|
714
|
+
url: "ws://system-2:3001",
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Simulate peer sending a message targeting local agent-b
|
|
718
|
+
expect(peerMessageHandlers).toHaveLength(1);
|
|
719
|
+
peerMessageHandlers[0]({
|
|
720
|
+
from: "agent-c",
|
|
721
|
+
payload: { type: "text", text: "Hello from system-2!" },
|
|
722
|
+
meta: {
|
|
723
|
+
targetAgent: "agent-b",
|
|
724
|
+
senderId: "agent-c",
|
|
725
|
+
sourceSystem: "system-2",
|
|
726
|
+
subject: "Cross-system greeting",
|
|
727
|
+
importance: "high",
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Wait for async routeMessage to complete
|
|
732
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
733
|
+
|
|
734
|
+
// Verify the message landed in agent-b's inbox
|
|
735
|
+
const inbox = storage.getInbox("agent-b");
|
|
736
|
+
expect(inbox).toHaveLength(1);
|
|
737
|
+
expect(inbox[0].sender_id).toBe("agent-c@system-2");
|
|
738
|
+
expect(inbox[0].content).toEqual({
|
|
739
|
+
type: "text",
|
|
740
|
+
text: "Hello from system-2!",
|
|
741
|
+
});
|
|
742
|
+
expect(inbox[0].subject).toBe("Cross-system greeting");
|
|
743
|
+
expect(inbox[0].importance).toBe("high");
|
|
744
|
+
|
|
745
|
+
// Verify recipient is agent-b and is marked delivered
|
|
746
|
+
expect(inbox[0].recipients[0].agent_id).toBe("agent-b");
|
|
747
|
+
expect(inbox[0].recipients[0].delivered_at).toBeDefined();
|
|
748
|
+
|
|
749
|
+
await transportFederation.destroy();
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it("should round-trip: send outbound with meta, receive inbound with meta", async () => {
|
|
753
|
+
storage.putAgent(makeAgent("agent-a"));
|
|
754
|
+
storage.putAgent(makeAgent("agent-b"));
|
|
755
|
+
|
|
756
|
+
const sentMessages: Array<{
|
|
757
|
+
to: { agentId?: string; scope?: string };
|
|
758
|
+
payload: unknown;
|
|
759
|
+
meta?: Record<string, unknown>;
|
|
760
|
+
}> = [];
|
|
761
|
+
|
|
762
|
+
const mockSdkClass: MapAgentConnectionClass = {
|
|
763
|
+
connect: async () => ({
|
|
764
|
+
send: async (to, payload, meta) => {
|
|
765
|
+
sentMessages.push({ to, payload, meta });
|
|
766
|
+
},
|
|
767
|
+
onMessage: () => {},
|
|
768
|
+
disconnect: async () => {},
|
|
769
|
+
}),
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const transportFederation = new ConnectionManager(
|
|
773
|
+
events,
|
|
774
|
+
{
|
|
775
|
+
systemId: "system-1",
|
|
776
|
+
trust: {
|
|
777
|
+
allowedServers: ["system-2"],
|
|
778
|
+
scopePermissions: {},
|
|
779
|
+
requireAuth: false,
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{ sdkClass: mockSdkClass }
|
|
783
|
+
);
|
|
784
|
+
router.setFederation(transportFederation);
|
|
785
|
+
|
|
786
|
+
await transportFederation.federate({
|
|
787
|
+
systemId: "system-2",
|
|
788
|
+
url: "ws://system-2:3001",
|
|
789
|
+
});
|
|
790
|
+
transportFederation.routing.updateFromExposure("system-2", [
|
|
791
|
+
{ agentId: "agent-c" },
|
|
792
|
+
]);
|
|
793
|
+
|
|
794
|
+
// Send a message with subject and thread
|
|
795
|
+
await router.routeMessage({
|
|
796
|
+
from: "agent-a",
|
|
797
|
+
to: "agent-c@system-2",
|
|
798
|
+
payload: "Thread message",
|
|
799
|
+
subject: "Important thread",
|
|
800
|
+
threadTag: "thread-1",
|
|
801
|
+
importance: "high",
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Verify the outbound meta includes targetAgent and all fields
|
|
805
|
+
expect(sentMessages).toHaveLength(1);
|
|
806
|
+
expect(sentMessages[0].to.agentId).toBe("agent-c");
|
|
807
|
+
expect(sentMessages[0].meta?.targetAgent).toBe("agent-c");
|
|
808
|
+
expect(sentMessages[0].meta?.senderId).toBe("agent-a");
|
|
809
|
+
expect(sentMessages[0].meta?.sourceSystem).toBe("system-1");
|
|
810
|
+
expect(sentMessages[0].meta?.subject).toBe("Important thread");
|
|
811
|
+
expect(sentMessages[0].meta?.threadTag).toBe("thread-1");
|
|
812
|
+
expect(sentMessages[0].meta?.importance).toBe("high");
|
|
813
|
+
|
|
814
|
+
await transportFederation.destroy();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("should handle mixed local and remote recipients", async () => {
|
|
818
|
+
storage.putAgent(makeAgent("agent-a"));
|
|
819
|
+
storage.putAgent(makeAgent("agent-b"));
|
|
820
|
+
|
|
821
|
+
await federation.federate({
|
|
822
|
+
systemId: "system-2",
|
|
823
|
+
url: "ws://system-2:3001",
|
|
824
|
+
});
|
|
825
|
+
federation.routing.updateFromExposure("system-2", [
|
|
826
|
+
{ agentId: "agent-c" },
|
|
827
|
+
]);
|
|
828
|
+
|
|
829
|
+
const routeSpy = vi.fn();
|
|
830
|
+
events.on("federation.route", routeSpy);
|
|
831
|
+
|
|
832
|
+
const msg = await router.routeMessage({
|
|
833
|
+
from: "agent-a",
|
|
834
|
+
to: [
|
|
835
|
+
{ agent_id: "agent-b", kind: "to" },
|
|
836
|
+
{ agent_id: "agent-c@system-2", kind: "cc" },
|
|
837
|
+
],
|
|
838
|
+
payload: "Hello everyone",
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// Local recipient gets delivered_at
|
|
842
|
+
const localRecipient = msg.recipients.find(
|
|
843
|
+
(r) => r.agent_id === "agent-b"
|
|
844
|
+
);
|
|
845
|
+
expect(localRecipient?.delivered_at).toBeDefined();
|
|
846
|
+
|
|
847
|
+
// Remote recipient gets delivered_at from federation route (synchronous)
|
|
848
|
+
const remoteRecipient = msg.recipients.find(
|
|
849
|
+
(r) => r.agent_id === "agent-c@system-2"
|
|
850
|
+
);
|
|
851
|
+
expect(remoteRecipient?.delivered_at).toBeDefined();
|
|
852
|
+
|
|
853
|
+
// Federation route event should have fired for the remote recipient
|
|
854
|
+
expect(routeSpy).toHaveBeenCalled();
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
});
|