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,744 @@
|
|
|
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 type {
|
|
7
|
+
MapConnection,
|
|
8
|
+
MapAgentConnectionClass,
|
|
9
|
+
IncomingMapMessage,
|
|
10
|
+
} from "../../src/map/map-client.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// MockMapServer — in-process MAP server that brokers messages between
|
|
14
|
+
// connected "agents" (which are really federation gateways).
|
|
15
|
+
//
|
|
16
|
+
// Each call to connect() returns a MapConnection. When a connection calls
|
|
17
|
+
// send({ agentId }, payload, meta), the server delivers the message to the
|
|
18
|
+
// connection whose `name` matches `agentId` (or whose `peerSystemId` matches).
|
|
19
|
+
// This simulates real MAP message routing between two systems.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
interface ServerConnection {
|
|
23
|
+
name: string;
|
|
24
|
+
peerSystemId?: string;
|
|
25
|
+
systemId?: string;
|
|
26
|
+
handler: ((msg: IncomingMapMessage) => void) | null;
|
|
27
|
+
disconnected: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MockMapServer {
|
|
31
|
+
readonly connections: ServerConnection[] = [];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an SDK class bound to this server instance.
|
|
35
|
+
* Each call to sdkClass.connect(url, opts) registers a connection.
|
|
36
|
+
*/
|
|
37
|
+
createSdkClass(): MapAgentConnectionClass {
|
|
38
|
+
const server = this;
|
|
39
|
+
return {
|
|
40
|
+
connect: async (
|
|
41
|
+
_url: string,
|
|
42
|
+
opts: Record<string, unknown>
|
|
43
|
+
): Promise<MapConnection> => {
|
|
44
|
+
const name = opts.name as string;
|
|
45
|
+
const metadata = opts.metadata as Record<string, unknown> | undefined;
|
|
46
|
+
|
|
47
|
+
const serverConn: ServerConnection = {
|
|
48
|
+
name,
|
|
49
|
+
peerSystemId: metadata?.peerSystemId as string | undefined,
|
|
50
|
+
systemId: metadata?.systemId as string | undefined,
|
|
51
|
+
handler: null,
|
|
52
|
+
disconnected: false,
|
|
53
|
+
};
|
|
54
|
+
server.connections.push(serverConn);
|
|
55
|
+
|
|
56
|
+
const conn: MapConnection = {
|
|
57
|
+
send: async (
|
|
58
|
+
to: { agentId?: string; scope?: string },
|
|
59
|
+
payload: unknown,
|
|
60
|
+
meta?: Record<string, unknown>
|
|
61
|
+
): Promise<void> => {
|
|
62
|
+
if (serverConn.disconnected) {
|
|
63
|
+
throw new Error("Connection closed");
|
|
64
|
+
}
|
|
65
|
+
// Check if any target is reachable — if not, throw
|
|
66
|
+
const delivered = server.deliver(name, to, payload, meta);
|
|
67
|
+
if (!delivered) {
|
|
68
|
+
throw new Error("No reachable target");
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onMessage: (handler: (msg: IncomingMapMessage) => void): void => {
|
|
72
|
+
serverConn.handler = handler;
|
|
73
|
+
},
|
|
74
|
+
disconnect: async (): Promise<void> => {
|
|
75
|
+
serverConn.disconnected = true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return conn;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Route a message from `senderName` to the appropriate connection.
|
|
86
|
+
*
|
|
87
|
+
* Routing logic (mimics MAP server behavior):
|
|
88
|
+
* 1. If to.agentId matches a connection's `name` → deliver there
|
|
89
|
+
* 2. If to.agentId matches a connection's `peerSystemId` → deliver there
|
|
90
|
+
* 3. Otherwise deliver to ALL connections except sender (broadcast)
|
|
91
|
+
*/
|
|
92
|
+
deliver(
|
|
93
|
+
senderName: string,
|
|
94
|
+
to: { agentId?: string; scope?: string },
|
|
95
|
+
payload: unknown,
|
|
96
|
+
meta?: Record<string, unknown>
|
|
97
|
+
): boolean {
|
|
98
|
+
const targets = this.findTargets(senderName, to);
|
|
99
|
+
let delivered = false;
|
|
100
|
+
for (const target of targets) {
|
|
101
|
+
if (target.handler && !target.disconnected) {
|
|
102
|
+
target.handler({
|
|
103
|
+
from: senderName,
|
|
104
|
+
payload,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
meta: meta as IncomingMapMessage["meta"],
|
|
107
|
+
});
|
|
108
|
+
delivered = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return delivered;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private findTargets(
|
|
115
|
+
senderName: string,
|
|
116
|
+
to: { agentId?: string; scope?: string }
|
|
117
|
+
): ServerConnection[] {
|
|
118
|
+
if (to.agentId) {
|
|
119
|
+
// Direct delivery: find connection by name or peerSystemId
|
|
120
|
+
const direct = this.connections.find(
|
|
121
|
+
(c) =>
|
|
122
|
+
!c.disconnected &&
|
|
123
|
+
c.name !== senderName &&
|
|
124
|
+
(c.name === to.agentId || c.peerSystemId === to.agentId)
|
|
125
|
+
);
|
|
126
|
+
if (direct) return [direct];
|
|
127
|
+
|
|
128
|
+
// Fall back: find connection whose systemId matches the target
|
|
129
|
+
// (the target agent is on that system)
|
|
130
|
+
const bySystem = this.connections.find(
|
|
131
|
+
(c) =>
|
|
132
|
+
!c.disconnected &&
|
|
133
|
+
c.name !== senderName &&
|
|
134
|
+
c.systemId === to.agentId
|
|
135
|
+
);
|
|
136
|
+
if (bySystem) return [bySystem];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Broadcast: deliver to all non-sender connections
|
|
140
|
+
return this.connections.filter(
|
|
141
|
+
(c) => !c.disconnected && c.name !== senderName
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Disconnect a specific connection by systemId */
|
|
146
|
+
disconnectSystem(systemId: string): void {
|
|
147
|
+
for (const conn of this.connections) {
|
|
148
|
+
if (conn.systemId === systemId) {
|
|
149
|
+
conn.disconnected = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Reconnect a specific connection by systemId */
|
|
155
|
+
reconnectSystem(systemId: string): void {
|
|
156
|
+
for (const conn of this.connections) {
|
|
157
|
+
if (conn.systemId === systemId) {
|
|
158
|
+
conn.disconnected = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get activeConnections(): number {
|
|
164
|
+
return this.connections.filter((c) => !c.disconnected).length;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Helper: create a fully-wired "system" (storage + router + federation)
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
interface TestSystem {
|
|
173
|
+
name: string;
|
|
174
|
+
storage: InMemoryStorage;
|
|
175
|
+
events: EventEmitter;
|
|
176
|
+
router: MessageRouter;
|
|
177
|
+
federation: ConnectionManager;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createTestSystem(
|
|
181
|
+
name: string,
|
|
182
|
+
sdkClass: MapAgentConnectionClass,
|
|
183
|
+
opts: {
|
|
184
|
+
allowedServers: string[];
|
|
185
|
+
strategy?: "table" | "broadcast" | "hierarchical";
|
|
186
|
+
upstream?: string[];
|
|
187
|
+
}
|
|
188
|
+
): TestSystem {
|
|
189
|
+
const storage = new InMemoryStorage();
|
|
190
|
+
const events = new EventEmitter();
|
|
191
|
+
const router = new MessageRouter(storage, events, "default");
|
|
192
|
+
|
|
193
|
+
const federation = new ConnectionManager(
|
|
194
|
+
events,
|
|
195
|
+
{
|
|
196
|
+
systemId: name,
|
|
197
|
+
trust: {
|
|
198
|
+
allowedServers: opts.allowedServers,
|
|
199
|
+
scopePermissions: {},
|
|
200
|
+
requireAuth: false,
|
|
201
|
+
},
|
|
202
|
+
routing: {
|
|
203
|
+
strategy: opts.strategy ?? "table",
|
|
204
|
+
...(opts.upstream ? { upstream: opts.upstream } : {}),
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
sdkClass,
|
|
209
|
+
onIncomingMessage: ({ from, peerId, payload, meta }) => {
|
|
210
|
+
const targetAgent = (meta?.targetAgent as string) ?? from;
|
|
211
|
+
router
|
|
212
|
+
.routeMessage({
|
|
213
|
+
from: `${from}@${peerId}`,
|
|
214
|
+
to: targetAgent,
|
|
215
|
+
payload,
|
|
216
|
+
scope: meta?.scope as string | undefined,
|
|
217
|
+
subject: meta?.subject as string | undefined,
|
|
218
|
+
importance: meta?.importance as "high" | "normal" | undefined,
|
|
219
|
+
threadTag: meta?.threadTag as string | undefined,
|
|
220
|
+
inReplyTo: meta?.inReplyTo as string | undefined,
|
|
221
|
+
metadata: meta,
|
|
222
|
+
})
|
|
223
|
+
.catch(() => {
|
|
224
|
+
// Silently handle routing errors in tests
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
router.setFederation(federation);
|
|
230
|
+
|
|
231
|
+
return { name, storage, events, router, federation };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Tests
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
describe("Federation SDK Integration (two-system)", () => {
|
|
239
|
+
let mapServer: MockMapServer;
|
|
240
|
+
let system1: TestSystem;
|
|
241
|
+
let system2: TestSystem;
|
|
242
|
+
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
mapServer = new MockMapServer();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
afterEach(async () => {
|
|
248
|
+
if (system1) await system1.federation.destroy();
|
|
249
|
+
if (system2) await system2.federation.destroy();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Helper: set up two systems federated with each other through the mock server.
|
|
254
|
+
*/
|
|
255
|
+
async function setupTwoSystems(opts?: {
|
|
256
|
+
strategy1?: "table" | "broadcast" | "hierarchical";
|
|
257
|
+
strategy2?: "table" | "broadcast" | "hierarchical";
|
|
258
|
+
}) {
|
|
259
|
+
const sdk = mapServer.createSdkClass();
|
|
260
|
+
|
|
261
|
+
system1 = createTestSystem("system-1", sdk, {
|
|
262
|
+
allowedServers: ["system-2"],
|
|
263
|
+
strategy: opts?.strategy1,
|
|
264
|
+
});
|
|
265
|
+
system2 = createTestSystem("system-2", sdk, {
|
|
266
|
+
allowedServers: ["system-1"],
|
|
267
|
+
strategy: opts?.strategy2,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Register local agents
|
|
271
|
+
system1.storage.putAgent({
|
|
272
|
+
agent_id: "alice",
|
|
273
|
+
display_name: "Alice",
|
|
274
|
+
scope: "default",
|
|
275
|
+
status: "active",
|
|
276
|
+
metadata: {},
|
|
277
|
+
registered_at: new Date().toISOString(),
|
|
278
|
+
last_active_at: new Date().toISOString(),
|
|
279
|
+
});
|
|
280
|
+
system2.storage.putAgent({
|
|
281
|
+
agent_id: "bob",
|
|
282
|
+
display_name: "Bob",
|
|
283
|
+
scope: "default",
|
|
284
|
+
status: "active",
|
|
285
|
+
metadata: {},
|
|
286
|
+
registered_at: new Date().toISOString(),
|
|
287
|
+
last_active_at: new Date().toISOString(),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Federate: each system connects to the other
|
|
291
|
+
await system1.federation.federate({
|
|
292
|
+
systemId: "system-2",
|
|
293
|
+
url: "ws://mock-server/system-2",
|
|
294
|
+
});
|
|
295
|
+
await system2.federation.federate({
|
|
296
|
+
systemId: "system-1",
|
|
297
|
+
url: "ws://mock-server/system-1",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Expose agents in routing tables
|
|
301
|
+
system1.federation.routing.updateFromExposure("system-2", [
|
|
302
|
+
{ agentId: "bob" },
|
|
303
|
+
]);
|
|
304
|
+
system2.federation.routing.updateFromExposure("system-1", [
|
|
305
|
+
{ agentId: "alice" },
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
describe("direct federation send (system-1 → system-2)", () => {
|
|
310
|
+
it("should deliver a message from alice@system-1 to bob@system-2", async () => {
|
|
311
|
+
await setupTwoSystems();
|
|
312
|
+
|
|
313
|
+
const msg = await system1.router.routeMessage({
|
|
314
|
+
from: "alice",
|
|
315
|
+
to: "bob@system-2",
|
|
316
|
+
payload: "Hello Bob, this is Alice!",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Message stored on system-1 (sender side)
|
|
320
|
+
expect(system1.storage.getMessage(msg.id)).toBeDefined();
|
|
321
|
+
|
|
322
|
+
// Note: federation routing is fire-and-forget from routeMessage's perspective,
|
|
323
|
+
// but our mock transport is synchronous, so delivery happens inline.
|
|
324
|
+
// The delivered_at is set on the message object returned by route(), but
|
|
325
|
+
// routeMessage doesn't wait for route() — so we check the receiver side.
|
|
326
|
+
|
|
327
|
+
// Message delivered to system-2 (receiver side) — in bob's inbox
|
|
328
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
329
|
+
expect(bobInbox).toHaveLength(1);
|
|
330
|
+
expect(bobInbox[0].sender_id).toBe("system-1@system-1");
|
|
331
|
+
expect(bobInbox[0].content).toEqual({
|
|
332
|
+
type: "text",
|
|
333
|
+
text: "Hello Bob, this is Alice!",
|
|
334
|
+
});
|
|
335
|
+
// Verify metadata propagated
|
|
336
|
+
expect(bobInbox[0].metadata?.targetAgent).toBe("bob");
|
|
337
|
+
expect(bobInbox[0].metadata?.senderId).toBe("alice");
|
|
338
|
+
expect(bobInbox[0].metadata?.sourceSystem).toBe("system-1");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should preserve subject, importance, and threadTag across systems", async () => {
|
|
342
|
+
await setupTwoSystems();
|
|
343
|
+
|
|
344
|
+
await system1.router.routeMessage({
|
|
345
|
+
from: "alice",
|
|
346
|
+
to: "bob@system-2",
|
|
347
|
+
payload: "Urgent thread message",
|
|
348
|
+
subject: "Project Alpha",
|
|
349
|
+
importance: "high",
|
|
350
|
+
threadTag: "alpha-thread",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
354
|
+
expect(bobInbox).toHaveLength(1);
|
|
355
|
+
expect(bobInbox[0].subject).toBe("Project Alpha");
|
|
356
|
+
expect(bobInbox[0].importance).toBe("high");
|
|
357
|
+
expect(bobInbox[0].thread_tag).toBe("alpha-thread");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("bidirectional messaging", () => {
|
|
362
|
+
it("should support alice → bob then bob → alice", async () => {
|
|
363
|
+
await setupTwoSystems();
|
|
364
|
+
|
|
365
|
+
// Alice sends to Bob
|
|
366
|
+
await system1.router.routeMessage({
|
|
367
|
+
from: "alice",
|
|
368
|
+
to: "bob@system-2",
|
|
369
|
+
payload: "Hey Bob!",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Bob sends to Alice
|
|
373
|
+
await system2.router.routeMessage({
|
|
374
|
+
from: "bob",
|
|
375
|
+
to: "alice@system-1",
|
|
376
|
+
payload: "Hey Alice!",
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Both inboxes should have messages
|
|
380
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
381
|
+
expect(bobInbox).toHaveLength(1);
|
|
382
|
+
expect(bobInbox[0].content).toEqual({
|
|
383
|
+
type: "text",
|
|
384
|
+
text: "Hey Bob!",
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const aliceInbox = system1.storage.getInbox("alice");
|
|
388
|
+
expect(aliceInbox).toHaveLength(1);
|
|
389
|
+
expect(aliceInbox[0].content).toEqual({
|
|
390
|
+
type: "text",
|
|
391
|
+
text: "Hey Alice!",
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should support multiple messages in both directions", async () => {
|
|
396
|
+
await setupTwoSystems();
|
|
397
|
+
|
|
398
|
+
await system1.router.routeMessage({
|
|
399
|
+
from: "alice",
|
|
400
|
+
to: "bob@system-2",
|
|
401
|
+
payload: "Message 1",
|
|
402
|
+
});
|
|
403
|
+
await system2.router.routeMessage({
|
|
404
|
+
from: "bob",
|
|
405
|
+
to: "alice@system-1",
|
|
406
|
+
payload: "Message 2",
|
|
407
|
+
});
|
|
408
|
+
await system1.router.routeMessage({
|
|
409
|
+
from: "alice",
|
|
410
|
+
to: "bob@system-2",
|
|
411
|
+
payload: "Message 3",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(system2.storage.getInbox("bob")).toHaveLength(2);
|
|
415
|
+
expect(system1.storage.getInbox("alice")).toHaveLength(1);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("offline peer — queue and flush with transport", () => {
|
|
420
|
+
it("should queue messages when peer transport is down, then flush on reconnect", async () => {
|
|
421
|
+
await setupTwoSystems();
|
|
422
|
+
|
|
423
|
+
// Verify initial connectivity
|
|
424
|
+
expect(system1.federation.hasTransport("system-2")).toBe(true);
|
|
425
|
+
|
|
426
|
+
// Disconnect system-2's connections on the server side.
|
|
427
|
+
// This makes conn.send() throw "No reachable target" because
|
|
428
|
+
// deliver() returns false when target connections are disconnected.
|
|
429
|
+
mapServer.disconnectSystem("system-2");
|
|
430
|
+
|
|
431
|
+
// Alice tries to send — transport send fails, message gets queued
|
|
432
|
+
const msg1 = {
|
|
433
|
+
id: "queued-1",
|
|
434
|
+
scope: "default",
|
|
435
|
+
sender_id: "alice",
|
|
436
|
+
recipients: [{ agent_id: "bob@system-2", kind: "to" as const }],
|
|
437
|
+
content: { type: "text" as const, text: "Are you there?" },
|
|
438
|
+
importance: "normal" as const,
|
|
439
|
+
metadata: {},
|
|
440
|
+
created_at: new Date().toISOString(),
|
|
441
|
+
};
|
|
442
|
+
const result = await system1.federation.route(msg1);
|
|
443
|
+
|
|
444
|
+
// Message should be queued (send threw because server couldn't deliver)
|
|
445
|
+
expect(result.delivered).toBe(false);
|
|
446
|
+
expect(result.queued).toBe(true);
|
|
447
|
+
expect(system1.federation.queue.size("system-2")).toBe(1);
|
|
448
|
+
|
|
449
|
+
// Bob's inbox should be empty (message didn't get through)
|
|
450
|
+
expect(system2.storage.getInbox("bob")).toHaveLength(0);
|
|
451
|
+
|
|
452
|
+
// Reconnect system-2 on server side
|
|
453
|
+
mapServer.reconnectSystem("system-2");
|
|
454
|
+
|
|
455
|
+
// Re-federate to trigger flush (disconnect + federate = reconnect)
|
|
456
|
+
await system1.federation.disconnect("system-2");
|
|
457
|
+
await system1.federation.federate({
|
|
458
|
+
systemId: "system-2",
|
|
459
|
+
url: "ws://mock-server/system-2",
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// The flush handler re-routes queued messages synchronously.
|
|
463
|
+
// Wait a tick for the async route() calls to resolve.
|
|
464
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
465
|
+
|
|
466
|
+
// Queue should be drained
|
|
467
|
+
expect(system1.federation.queue.size("system-2")).toBe(0);
|
|
468
|
+
|
|
469
|
+
// Bob should now have the message
|
|
470
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
471
|
+
expect(bobInbox).toHaveLength(1);
|
|
472
|
+
expect(bobInbox[0].content).toEqual({
|
|
473
|
+
type: "text",
|
|
474
|
+
text: "Are you there?",
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe("broadcast strategy with real transport", () => {
|
|
480
|
+
// For system-qualified addresses (bob@system-2) where the target system IS a
|
|
481
|
+
// connected peer, delivery goes directly via transport (no broadcast needed).
|
|
482
|
+
// Broadcast/hierarchical strategies activate when the resolved peer has no
|
|
483
|
+
// active link — e.g., targeting a system we're not directly connected to.
|
|
484
|
+
//
|
|
485
|
+
// These tests verify broadcast transport by:
|
|
486
|
+
// 1. Testing system-qualified delivery to multiple peers (direct transport)
|
|
487
|
+
// 2. Verifying the broadcast event + transport work together on crafted messages
|
|
488
|
+
|
|
489
|
+
let system3: TestSystem;
|
|
490
|
+
|
|
491
|
+
afterEach(async () => {
|
|
492
|
+
if (system3) await system3.federation.destroy();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("should deliver to multiple peers via real transport (fan-out pattern)", async () => {
|
|
496
|
+
const sdk = mapServer.createSdkClass();
|
|
497
|
+
|
|
498
|
+
system1 = createTestSystem("system-1", sdk, {
|
|
499
|
+
allowedServers: ["system-2", "system-3"],
|
|
500
|
+
});
|
|
501
|
+
system2 = createTestSystem("system-2", sdk, {
|
|
502
|
+
allowedServers: ["system-1"],
|
|
503
|
+
});
|
|
504
|
+
system3 = createTestSystem("system-3", sdk, {
|
|
505
|
+
allowedServers: ["system-1"],
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Register agents on each system
|
|
509
|
+
system1.storage.putAgent({
|
|
510
|
+
agent_id: "alice",
|
|
511
|
+
display_name: "Alice",
|
|
512
|
+
scope: "default",
|
|
513
|
+
status: "active",
|
|
514
|
+
metadata: {},
|
|
515
|
+
registered_at: new Date().toISOString(),
|
|
516
|
+
last_active_at: new Date().toISOString(),
|
|
517
|
+
});
|
|
518
|
+
system2.storage.putAgent({
|
|
519
|
+
agent_id: "bob",
|
|
520
|
+
display_name: "Bob",
|
|
521
|
+
scope: "default",
|
|
522
|
+
status: "active",
|
|
523
|
+
metadata: {},
|
|
524
|
+
registered_at: new Date().toISOString(),
|
|
525
|
+
last_active_at: new Date().toISOString(),
|
|
526
|
+
});
|
|
527
|
+
system3.storage.putAgent({
|
|
528
|
+
agent_id: "charlie",
|
|
529
|
+
display_name: "Charlie",
|
|
530
|
+
scope: "default",
|
|
531
|
+
status: "active",
|
|
532
|
+
metadata: {},
|
|
533
|
+
registered_at: new Date().toISOString(),
|
|
534
|
+
last_active_at: new Date().toISOString(),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Federate all systems
|
|
538
|
+
await system1.federation.federate({
|
|
539
|
+
systemId: "system-2",
|
|
540
|
+
url: "ws://mock-server/system-2",
|
|
541
|
+
});
|
|
542
|
+
await system1.federation.federate({
|
|
543
|
+
systemId: "system-3",
|
|
544
|
+
url: "ws://mock-server/system-3",
|
|
545
|
+
});
|
|
546
|
+
await system2.federation.federate({
|
|
547
|
+
systemId: "system-1",
|
|
548
|
+
url: "ws://mock-server/system-1",
|
|
549
|
+
});
|
|
550
|
+
await system3.federation.federate({
|
|
551
|
+
systemId: "system-1",
|
|
552
|
+
url: "ws://mock-server/system-1",
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Send two messages: one to each remote system
|
|
556
|
+
await system1.router.routeMessage({
|
|
557
|
+
from: "alice",
|
|
558
|
+
to: "bob@system-2",
|
|
559
|
+
payload: "Hello Bob!",
|
|
560
|
+
});
|
|
561
|
+
await system1.router.routeMessage({
|
|
562
|
+
from: "alice",
|
|
563
|
+
to: "charlie@system-3",
|
|
564
|
+
payload: "Hello Charlie!",
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Both should have received their messages
|
|
568
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
569
|
+
expect(bobInbox).toHaveLength(1);
|
|
570
|
+
expect(bobInbox[0].content).toEqual({
|
|
571
|
+
type: "text",
|
|
572
|
+
text: "Hello Bob!",
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const charlieInbox = system3.storage.getInbox("charlie");
|
|
576
|
+
expect(charlieInbox).toHaveLength(1);
|
|
577
|
+
expect(charlieInbox[0].content).toEqual({
|
|
578
|
+
type: "text",
|
|
579
|
+
text: "Hello Charlie!",
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("should send to all connected peers when broadcast strategy handles unknown route", async () => {
|
|
584
|
+
const sdk = mapServer.createSdkClass();
|
|
585
|
+
|
|
586
|
+
// System-1 uses broadcast strategy
|
|
587
|
+
system1 = createTestSystem("system-1", sdk, {
|
|
588
|
+
allowedServers: ["system-2", "system-3"],
|
|
589
|
+
strategy: "broadcast",
|
|
590
|
+
});
|
|
591
|
+
system2 = createTestSystem("system-2", sdk, {
|
|
592
|
+
allowedServers: ["system-1"],
|
|
593
|
+
});
|
|
594
|
+
system3 = createTestSystem("system-3", sdk, {
|
|
595
|
+
allowedServers: ["system-1"],
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
system2.storage.putAgent({
|
|
599
|
+
agent_id: "bob",
|
|
600
|
+
display_name: "Bob",
|
|
601
|
+
scope: "default",
|
|
602
|
+
status: "active",
|
|
603
|
+
metadata: {},
|
|
604
|
+
registered_at: new Date().toISOString(),
|
|
605
|
+
last_active_at: new Date().toISOString(),
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Federate
|
|
609
|
+
await system1.federation.federate({
|
|
610
|
+
systemId: "system-2",
|
|
611
|
+
url: "ws://mock-server/system-2",
|
|
612
|
+
});
|
|
613
|
+
await system1.federation.federate({
|
|
614
|
+
systemId: "system-3",
|
|
615
|
+
url: "ws://mock-server/system-3",
|
|
616
|
+
});
|
|
617
|
+
await system2.federation.federate({
|
|
618
|
+
systemId: "system-1",
|
|
619
|
+
url: "ws://mock-server/system-1",
|
|
620
|
+
});
|
|
621
|
+
await system3.federation.federate({
|
|
622
|
+
systemId: "system-1",
|
|
623
|
+
url: "ws://mock-server/system-1",
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Craft a message that triggers handleUnknownRoute directly.
|
|
627
|
+
// Use a recipient with a system that resolves but has no peer link,
|
|
628
|
+
// which queues it. However, for broadcast to trigger, resolveRoute
|
|
629
|
+
// must return null — which only happens for agent-only (non-remote)
|
|
630
|
+
// addresses. So we test via direct route() call with a system-qualified
|
|
631
|
+
// address where the system IS a connected peer but the agent is NOT
|
|
632
|
+
// in the routing table. resolveRoute returns "system-2" (from addr.system),
|
|
633
|
+
// peer is connected, so it delivers directly via transport.
|
|
634
|
+
const msg = {
|
|
635
|
+
id: "broadcast-direct-1",
|
|
636
|
+
scope: "default",
|
|
637
|
+
sender_id: "alice",
|
|
638
|
+
recipients: [{ agent_id: "bob@system-2", kind: "to" as const }],
|
|
639
|
+
content: { type: "text" as const, text: "Direct via transport" },
|
|
640
|
+
importance: "normal" as const,
|
|
641
|
+
metadata: {},
|
|
642
|
+
created_at: new Date().toISOString(),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const result = await system1.federation.route(msg);
|
|
646
|
+
expect(result.delivered).toBe(true);
|
|
647
|
+
expect(result.peerId).toBe("system-2");
|
|
648
|
+
|
|
649
|
+
// Message should arrive at system-2 via real transport
|
|
650
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
651
|
+
expect(bobInbox).toHaveLength(1);
|
|
652
|
+
expect(bobInbox[0].content).toEqual({
|
|
653
|
+
type: "text",
|
|
654
|
+
text: "Direct via transport",
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
describe("transport identity verification", () => {
|
|
660
|
+
it("should connect with correct gateway metadata", async () => {
|
|
661
|
+
await setupTwoSystems();
|
|
662
|
+
|
|
663
|
+
// Verify system-1's connection to system-2
|
|
664
|
+
const sys1Conn = mapServer.connections.find(
|
|
665
|
+
(c) => c.systemId === "system-1" && c.peerSystemId === "system-2"
|
|
666
|
+
);
|
|
667
|
+
expect(sys1Conn).toBeDefined();
|
|
668
|
+
expect(sys1Conn!.name).toBe("system-1");
|
|
669
|
+
|
|
670
|
+
// Verify system-2's connection to system-1
|
|
671
|
+
const sys2Conn = mapServer.connections.find(
|
|
672
|
+
(c) => c.systemId === "system-2" && c.peerSystemId === "system-1"
|
|
673
|
+
);
|
|
674
|
+
expect(sys2Conn).toBeDefined();
|
|
675
|
+
expect(sys2Conn!.name).toBe("system-2");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("should have 2 active connections after bilateral federation", async () => {
|
|
679
|
+
await setupTwoSystems();
|
|
680
|
+
expect(mapServer.activeConnections).toBe(2);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
describe("hasTransport reflects real connections", () => {
|
|
685
|
+
it("should report transport status accurately", async () => {
|
|
686
|
+
await setupTwoSystems();
|
|
687
|
+
|
|
688
|
+
expect(system1.federation.hasTransport("system-2")).toBe(true);
|
|
689
|
+
expect(system2.federation.hasTransport("system-1")).toBe(true);
|
|
690
|
+
|
|
691
|
+
// After disconnect, transport should be gone
|
|
692
|
+
await system1.federation.disconnect("system-2");
|
|
693
|
+
expect(system1.federation.hasTransport("system-2")).toBe(false);
|
|
694
|
+
|
|
695
|
+
// system-2 still has its connection to system-1
|
|
696
|
+
expect(system2.federation.hasTransport("system-1")).toBe(true);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
describe("message content fidelity", () => {
|
|
701
|
+
it("should preserve complex structured content across federation", async () => {
|
|
702
|
+
await setupTwoSystems();
|
|
703
|
+
|
|
704
|
+
const complexPayload = {
|
|
705
|
+
type: "structured",
|
|
706
|
+
blocks: [
|
|
707
|
+
{ type: "heading", text: "Report" },
|
|
708
|
+
{ type: "code", language: "typescript", text: "const x = 1;" },
|
|
709
|
+
{ type: "list", items: ["a", "b", "c"] },
|
|
710
|
+
],
|
|
711
|
+
attachments: [{ name: "data.json", size: 1024 }],
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
await system1.router.routeMessage({
|
|
715
|
+
from: "alice",
|
|
716
|
+
to: "bob@system-2",
|
|
717
|
+
payload: complexPayload,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
721
|
+
expect(bobInbox).toHaveLength(1);
|
|
722
|
+
// Content gets wrapped by normalizeContent — check the payload arrived
|
|
723
|
+
expect(bobInbox[0].content).toEqual(complexPayload);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("should handle string payloads (auto-wrapped to text content)", async () => {
|
|
727
|
+
await setupTwoSystems();
|
|
728
|
+
|
|
729
|
+
await system1.router.routeMessage({
|
|
730
|
+
from: "alice",
|
|
731
|
+
to: "bob@system-2",
|
|
732
|
+
payload: "Simple string message",
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const bobInbox = system2.storage.getInbox("bob");
|
|
736
|
+
expect(bobInbox).toHaveLength(1);
|
|
737
|
+
// normalizeContent wraps strings into { type: "text", text: "..." }
|
|
738
|
+
expect(bobInbox[0].content).toEqual({
|
|
739
|
+
type: "text",
|
|
740
|
+
text: "Simple string message",
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
});
|