claude-code-swarm 0.3.5 → 0.3.6
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 +1 -1
- package/.claude-plugin/run-agent-inbox-mcp.sh +22 -3
- package/.gitattributes +3 -0
- package/.opentasks/config.json +9 -0
- package/.opentasks/graph.jsonl +0 -0
- package/e2e/helpers/opentasks-daemon.mjs +149 -0
- package/e2e/tier6-live-inbox-flow.test.mjs +938 -0
- package/e2e/tier7-hooks.test.mjs +992 -0
- package/e2e/tier7-minimem.test.mjs +461 -0
- package/e2e/tier7-opentasks.test.mjs +513 -0
- package/e2e/tier7-skilltree.test.mjs +506 -0
- package/e2e/vitest.config.e2e.mjs +1 -1
- package/package.json +7 -3
- package/references/agent-inbox/src/index.ts +16 -2
- package/references/agent-inbox/src/ipc/ipc-server.ts +58 -0
- package/references/agent-inbox/src/mcp/mcp-proxy.ts +326 -0
- package/references/agent-inbox/src/types.ts +26 -0
- package/references/agent-inbox/test/ipc-new-commands.test.ts +200 -0
- package/references/agent-inbox/test/mcp-proxy.test.ts +191 -0
- package/scripts/bootstrap.mjs +8 -1
- package/scripts/map-hook.mjs +6 -2
- package/scripts/map-sidecar.mjs +19 -0
- package/scripts/team-loader.mjs +15 -8
- package/skills/swarm/SKILL.md +16 -22
- package/src/__tests__/agent-generator.test.mjs +9 -10
- package/src/__tests__/context-output.test.mjs +13 -14
- package/src/__tests__/e2e-inbox-integration.test.mjs +732 -0
- package/src/__tests__/e2e-live-inbox.test.mjs +597 -0
- package/src/__tests__/inbox-integration.test.mjs +298 -0
- package/src/__tests__/integration.test.mjs +12 -11
- package/src/__tests__/skilltree-client.test.mjs +47 -1
- package/src/agent-generator.mjs +79 -88
- package/src/bootstrap.mjs +24 -3
- package/src/context-output.mjs +238 -64
- package/src/index.mjs +2 -0
- package/src/sidecar-server.mjs +30 -0
- package/src/skilltree-client.mjs +50 -5
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 6: Live Agent Inbox Flow Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies the full inbox messaging pipeline with live Claude Code agents:
|
|
5
|
+
* 1. Sidecar starts with inbox enabled → inbox IPC socket appears
|
|
6
|
+
* 2. Agent spawned via /swarm registers in inbox storage (visible via list_agents)
|
|
7
|
+
* 3. External message sent to agent's inbox ID → agent receives it
|
|
8
|
+
* 4. Agent uses send_message MCP tool → message lands in real inbox storage
|
|
9
|
+
* 5. Threaded conversation between agents via inbox MCP tools
|
|
10
|
+
* 6. message.created events bridge to MAP server
|
|
11
|
+
*
|
|
12
|
+
* Gated behind LIVE_AGENT_TEST=1 (requires live Claude Code CLI + API key).
|
|
13
|
+
*
|
|
14
|
+
* Run:
|
|
15
|
+
* LIVE_AGENT_TEST=1 npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier6-live-inbox-flow.test.mjs
|
|
16
|
+
*
|
|
17
|
+
* Run specific group:
|
|
18
|
+
* LIVE_AGENT_TEST=1 npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier6-live-inbox-flow.test.mjs -t "single agent"
|
|
19
|
+
* LIVE_AGENT_TEST=1 npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier6-live-inbox-flow.test.mjs -t "team inbox"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
23
|
+
import fs from "fs";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { runClaude, CLI_AVAILABLE } from "./helpers/cli.mjs";
|
|
26
|
+
import { extractToolCalls, findToolCalls, getResult, getHookOutput } from "./helpers/assertions.mjs";
|
|
27
|
+
import { createWorkspace } from "./helpers/workspace.mjs";
|
|
28
|
+
import { cleanupWorkspace, waitFor } from "./helpers/cleanup.mjs";
|
|
29
|
+
import { MockMapServer } from "./helpers/map-mock-server.mjs";
|
|
30
|
+
import { startTestSidecar, sendCommand } from "./helpers/sidecar.mjs";
|
|
31
|
+
|
|
32
|
+
const LIVE = !!process.env.LIVE_AGENT_TEST;
|
|
33
|
+
const SHORT_TMPDIR = "/tmp";
|
|
34
|
+
|
|
35
|
+
// Check if agent-inbox is available
|
|
36
|
+
let agentInboxAvailable = false;
|
|
37
|
+
try {
|
|
38
|
+
await import("agent-inbox");
|
|
39
|
+
agentInboxAvailable = true;
|
|
40
|
+
} catch {
|
|
41
|
+
// Not installed
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Group 1: Single Agent — Inbox MCP round-trip with real storage
|
|
46
|
+
//
|
|
47
|
+
// Starts a real sidecar with inbox, then a live agent that sends a message
|
|
48
|
+
// via MCP and checks its own inbox. Verifies the message lands in real
|
|
49
|
+
// inbox storage (not isolated MCP storage).
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe.skipIf(!LIVE || !CLI_AVAILABLE || !agentInboxAvailable)(
|
|
53
|
+
"tier6: single agent inbox MCP → real storage round-trip",
|
|
54
|
+
{ timeout: 300_000 },
|
|
55
|
+
() => {
|
|
56
|
+
let mockServer;
|
|
57
|
+
let workspace;
|
|
58
|
+
let sidecar;
|
|
59
|
+
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
if (sidecar) sidecar.cleanup();
|
|
62
|
+
if (workspace) {
|
|
63
|
+
cleanupWorkspace(workspace.dir);
|
|
64
|
+
workspace.cleanup();
|
|
65
|
+
}
|
|
66
|
+
if (mockServer) await mockServer.stop();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("agent sends message via MCP → message appears in real inbox IPC storage", async () => {
|
|
70
|
+
mockServer = new MockMapServer();
|
|
71
|
+
await mockServer.start();
|
|
72
|
+
|
|
73
|
+
workspace = createWorkspace({
|
|
74
|
+
tmpdir: SHORT_TMPDIR, prefix: "s6-inbox-",
|
|
75
|
+
config: {
|
|
76
|
+
template: "gsd",
|
|
77
|
+
map: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
server: `ws://localhost:${mockServer.port}`,
|
|
80
|
+
sidecar: "session",
|
|
81
|
+
},
|
|
82
|
+
inbox: { enabled: true },
|
|
83
|
+
},
|
|
84
|
+
files: {
|
|
85
|
+
"README.md": "# Inbox Flow Test\n",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Start sidecar with inbox before the agent session
|
|
90
|
+
sidecar = await startTestSidecar({
|
|
91
|
+
workspaceDir: workspace.dir,
|
|
92
|
+
mockServerPort: mockServer.port,
|
|
93
|
+
inboxConfig: { enabled: true },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(sidecar.inboxReady).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Run a live agent that uses inbox MCP tools
|
|
99
|
+
const run = await runClaude(
|
|
100
|
+
'Use the agent-inbox MCP tools to do the following:\n' +
|
|
101
|
+
'1. Use send_message to send a message to agent "gsd-verifier" with body "Please verify task #100"\n' +
|
|
102
|
+
'2. Use send_message to send a message to agent "gsd-verifier" with body "Also check task #101" and threadTag "tasks-batch"\n' +
|
|
103
|
+
'3. Use check_inbox to check messages for agent "gsd-verifier"\n' +
|
|
104
|
+
'4. Use list_agents to list all registered agents\n' +
|
|
105
|
+
'5. Report what each tool returned',
|
|
106
|
+
{
|
|
107
|
+
cwd: workspace.dir,
|
|
108
|
+
maxBudgetUsd: 2.0,
|
|
109
|
+
maxTurns: 15,
|
|
110
|
+
timeout: 120_000,
|
|
111
|
+
label: "tier6-inbox-flow-single",
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const toolCalls = extractToolCalls(run.messages);
|
|
116
|
+
const toolNames = toolCalls.map((tc) => tc.name);
|
|
117
|
+
console.log("[tier6] inbox flow tool calls:", toolNames.join(", "));
|
|
118
|
+
|
|
119
|
+
// Verify the agent used inbox MCP tools
|
|
120
|
+
const sendCalls = toolCalls.filter((tc) => tc.name.includes("send_message"));
|
|
121
|
+
const checkCalls = toolCalls.filter((tc) => tc.name.includes("check_inbox"));
|
|
122
|
+
const listCalls = toolCalls.filter((tc) => tc.name.includes("list_agents"));
|
|
123
|
+
|
|
124
|
+
console.log(`[tier6] send_message: ${sendCalls.length}, check_inbox: ${checkCalls.length}, list_agents: ${listCalls.length}`);
|
|
125
|
+
|
|
126
|
+
expect(sendCalls.length).toBeGreaterThanOrEqual(1);
|
|
127
|
+
expect(checkCalls.length).toBeGreaterThanOrEqual(1);
|
|
128
|
+
|
|
129
|
+
// Now verify the message actually landed in real inbox storage via IPC
|
|
130
|
+
const inboxResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
131
|
+
action: "check_inbox",
|
|
132
|
+
agentId: "gsd-verifier",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log("[tier6] inbox IPC check_inbox response:", JSON.stringify(inboxResp));
|
|
136
|
+
|
|
137
|
+
if (inboxResp?.ok) {
|
|
138
|
+
// Messages sent by the agent via MCP should be in real storage
|
|
139
|
+
expect(inboxResp.messages.length).toBeGreaterThanOrEqual(1);
|
|
140
|
+
|
|
141
|
+
// Verify message content
|
|
142
|
+
const hasVerifyMsg = inboxResp.messages.some((m) =>
|
|
143
|
+
JSON.stringify(m).includes("verify") || JSON.stringify(m).includes("task")
|
|
144
|
+
);
|
|
145
|
+
console.log("[tier6] found verify message in real storage:", hasVerifyMsg);
|
|
146
|
+
} else {
|
|
147
|
+
console.log("[tier6] WARNING: inbox IPC not responding — MCP may be in standalone mode");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Verify no errors in the result
|
|
151
|
+
const result = getResult(run.messages);
|
|
152
|
+
expect(result?.is_error).toBeFalsy();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Group 2: External message → Agent inbox → Agent response
|
|
159
|
+
//
|
|
160
|
+
// Sends a message to the agent's inbox BEFORE the agent session starts,
|
|
161
|
+
// then verifies the agent receives it (via inject hook or MCP check_inbox).
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
describe.skipIf(!LIVE || !CLI_AVAILABLE || !agentInboxAvailable)(
|
|
165
|
+
"tier6: external message → agent inbox → agent processes",
|
|
166
|
+
{ timeout: 300_000 },
|
|
167
|
+
() => {
|
|
168
|
+
let mockServer;
|
|
169
|
+
let workspace;
|
|
170
|
+
let sidecar;
|
|
171
|
+
|
|
172
|
+
afterAll(async () => {
|
|
173
|
+
if (sidecar) sidecar.cleanup();
|
|
174
|
+
if (workspace) {
|
|
175
|
+
cleanupWorkspace(workspace.dir);
|
|
176
|
+
workspace.cleanup();
|
|
177
|
+
}
|
|
178
|
+
if (mockServer) await mockServer.stop();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("agent receives pre-seeded inbox message and responds to it", async () => {
|
|
182
|
+
mockServer = new MockMapServer();
|
|
183
|
+
await mockServer.start();
|
|
184
|
+
|
|
185
|
+
workspace = createWorkspace({
|
|
186
|
+
tmpdir: SHORT_TMPDIR, prefix: "s6-ext-",
|
|
187
|
+
config: {
|
|
188
|
+
template: "gsd",
|
|
189
|
+
map: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
server: `ws://localhost:${mockServer.port}`,
|
|
192
|
+
sidecar: "session",
|
|
193
|
+
},
|
|
194
|
+
inbox: { enabled: true },
|
|
195
|
+
},
|
|
196
|
+
files: {
|
|
197
|
+
"README.md": "# External Message Test\n",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Start sidecar with inbox
|
|
202
|
+
sidecar = await startTestSidecar({
|
|
203
|
+
workspaceDir: workspace.dir,
|
|
204
|
+
mockServerPort: mockServer.port,
|
|
205
|
+
inboxConfig: { enabled: true },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(sidecar.inboxReady).toBe(true);
|
|
209
|
+
|
|
210
|
+
// Pre-seed a message into the inbox for gsd-main (the inject hook checks this ID)
|
|
211
|
+
const seedResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
212
|
+
action: "send",
|
|
213
|
+
from: "external-coordinator",
|
|
214
|
+
to: "gsd-main",
|
|
215
|
+
payload: "PRIORITY: The deployment key is DELTA-4477. Acknowledge receipt.",
|
|
216
|
+
});
|
|
217
|
+
expect(seedResp?.ok).toBe(true);
|
|
218
|
+
console.log("[tier6] pre-seeded message:", seedResp?.messageId);
|
|
219
|
+
|
|
220
|
+
// Verify it's in the inbox before the agent session
|
|
221
|
+
const preCheck = await sendCommand(sidecar.inboxSocketPath, {
|
|
222
|
+
action: "check_inbox",
|
|
223
|
+
agentId: "gsd-main",
|
|
224
|
+
});
|
|
225
|
+
expect(preCheck?.ok).toBe(true);
|
|
226
|
+
expect(preCheck?.messages?.length).toBe(1);
|
|
227
|
+
|
|
228
|
+
// Run the agent — the inject hook should surface the message, or the agent
|
|
229
|
+
// can use check_inbox MCP tool
|
|
230
|
+
const run = await runClaude(
|
|
231
|
+
"Check for any external messages or inbox notifications. " +
|
|
232
|
+
"If you find any messages, report the full content including any codes or keys. " +
|
|
233
|
+
"Also use the agent-inbox check_inbox MCP tool to check for messages addressed to you.",
|
|
234
|
+
{
|
|
235
|
+
cwd: workspace.dir,
|
|
236
|
+
maxBudgetUsd: 2.0,
|
|
237
|
+
maxTurns: 10,
|
|
238
|
+
timeout: 120_000,
|
|
239
|
+
label: "tier6-inbox-external",
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const allText = run.stdout + run.stderr;
|
|
244
|
+
const hookOutput = getHookOutput(run.messages);
|
|
245
|
+
|
|
246
|
+
const mentionsKey = allText.includes("DELTA-4477") || hookOutput.includes("DELTA-4477");
|
|
247
|
+
const mentionsSender = allText.includes("external-coordinator") || hookOutput.includes("external-coordinator");
|
|
248
|
+
const mentionsMAP = allText.includes("[MAP]") || hookOutput.includes("[MAP]");
|
|
249
|
+
|
|
250
|
+
console.log(`[tier6] external — key: ${mentionsKey}, sender: ${mentionsSender}, MAP: ${mentionsMAP}`);
|
|
251
|
+
|
|
252
|
+
// At least one of these should be true — either the inject hook surfaced
|
|
253
|
+
// the message or the agent used check_inbox MCP tool
|
|
254
|
+
expect(mentionsKey || mentionsSender || mentionsMAP).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
260
|
+
// Group 3: Team inbox flow — /swarm spawns agents, agents message each other
|
|
261
|
+
//
|
|
262
|
+
// This is the full e2e test: /swarm launches a team, agents are registered
|
|
263
|
+
// in inbox, they exchange messages, and the MAP server sees inbox.message
|
|
264
|
+
// bridge events.
|
|
265
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe.skipIf(!LIVE || !CLI_AVAILABLE || !agentInboxAvailable)(
|
|
268
|
+
"tier6: team inbox flow — /swarm agents exchange messages",
|
|
269
|
+
{ timeout: 600_000 },
|
|
270
|
+
() => {
|
|
271
|
+
let mockServer;
|
|
272
|
+
let workspace;
|
|
273
|
+
|
|
274
|
+
afterAll(async () => {
|
|
275
|
+
if (workspace) {
|
|
276
|
+
cleanupWorkspace(workspace.dir);
|
|
277
|
+
workspace.cleanup();
|
|
278
|
+
}
|
|
279
|
+
if (mockServer) await mockServer.stop();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("agents spawned via /swarm are registered in inbox and can exchange messages", async () => {
|
|
283
|
+
mockServer = new MockMapServer();
|
|
284
|
+
await mockServer.start();
|
|
285
|
+
|
|
286
|
+
workspace = createWorkspace({
|
|
287
|
+
tmpdir: SHORT_TMPDIR, prefix: "s6-team-",
|
|
288
|
+
config: {
|
|
289
|
+
template: "gsd",
|
|
290
|
+
map: {
|
|
291
|
+
enabled: true,
|
|
292
|
+
server: `ws://localhost:${mockServer.port}`,
|
|
293
|
+
sidecar: "session",
|
|
294
|
+
},
|
|
295
|
+
inbox: { enabled: true },
|
|
296
|
+
},
|
|
297
|
+
files: {
|
|
298
|
+
"README.md": "# Team Inbox Flow Test\n",
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Run /swarm with a goal that encourages agent communication
|
|
303
|
+
const run = await runClaude(
|
|
304
|
+
'Run /swarm gsd with goal: Create a file called result.txt with "Team inbox works". ' +
|
|
305
|
+
'When coordinating with your team agents, use both SendMessage for quick coordination AND ' +
|
|
306
|
+
'agent-inbox send_message MCP tool for any messages that should be tracked/persistent. ' +
|
|
307
|
+
'Make sure each agent checks their inbox using agent-inbox check_inbox before starting work.',
|
|
308
|
+
{
|
|
309
|
+
cwd: workspace.dir,
|
|
310
|
+
maxBudgetUsd: 10.0,
|
|
311
|
+
maxTurns: 50,
|
|
312
|
+
timeout: 300_000,
|
|
313
|
+
label: "tier6-team-inbox-flow",
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const toolCalls = extractToolCalls(run.messages);
|
|
318
|
+
const toolNames = toolCalls.map((tc) => tc.name);
|
|
319
|
+
console.log("[tier6] team inbox flow tool calls:", toolNames.join(", "));
|
|
320
|
+
|
|
321
|
+
// ── Verify team was created ──
|
|
322
|
+
const teamCreates = findToolCalls(run.messages, "TeamCreate");
|
|
323
|
+
const agentCalls = findToolCalls(run.messages, "Agent");
|
|
324
|
+
console.log(`[tier6] TeamCreate: ${teamCreates.length}, Agent: ${agentCalls.length}`);
|
|
325
|
+
|
|
326
|
+
expect(teamCreates.length).toBeGreaterThan(0);
|
|
327
|
+
|
|
328
|
+
// ── Verify agents were spawned in MAP ──
|
|
329
|
+
if (agentCalls.length > 0) {
|
|
330
|
+
await new Promise((r) => setTimeout(r, 2000)); // Allow MAP events to settle
|
|
331
|
+
|
|
332
|
+
console.log(
|
|
333
|
+
"[tier6] MAP spawned agents:",
|
|
334
|
+
mockServer.spawnedAgents.map((a) => `${a.agentId} (${a.role})`).join(", ")
|
|
335
|
+
);
|
|
336
|
+
expect(mockServer.spawnedAgents.length).toBeGreaterThan(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Check for inbox MCP tool usage ──
|
|
340
|
+
const inboxToolCalls = toolCalls.filter((tc) =>
|
|
341
|
+
tc.name.includes("agent-inbox") || tc.name.includes("inbox")
|
|
342
|
+
);
|
|
343
|
+
const sendMsgCalls = inboxToolCalls.filter((tc) => tc.name.includes("send_message"));
|
|
344
|
+
const checkInboxCalls = inboxToolCalls.filter((tc) => tc.name.includes("check_inbox"));
|
|
345
|
+
const readThreadCalls = inboxToolCalls.filter((tc) => tc.name.includes("read_thread"));
|
|
346
|
+
const listAgentsCalls = inboxToolCalls.filter((tc) => tc.name.includes("list_agents"));
|
|
347
|
+
|
|
348
|
+
console.log(
|
|
349
|
+
`[tier6] inbox tools — send: ${sendMsgCalls.length}, check: ${checkInboxCalls.length}, ` +
|
|
350
|
+
`thread: ${readThreadCalls.length}, list: ${listAgentsCalls.length}`
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// At minimum, agents should have checked their inbox
|
|
354
|
+
// (Non-deterministic: LLM may or may not use inbox tools depending on prompt interpretation)
|
|
355
|
+
if (inboxToolCalls.length > 0) {
|
|
356
|
+
console.log("[tier6] agents DID use inbox MCP tools");
|
|
357
|
+
} else {
|
|
358
|
+
console.log("[tier6] WARNING: agents did not use inbox MCP tools (LLM non-deterministic)");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Check for inbox.message events on MAP (outbound bridge) ──
|
|
362
|
+
const inboxMessages = mockServer.sentMessages.filter(
|
|
363
|
+
(m) => m.payload?.type === "inbox.message"
|
|
364
|
+
);
|
|
365
|
+
console.log(`[tier6] inbox.message MAP events: ${inboxMessages.length}`);
|
|
366
|
+
|
|
367
|
+
if (inboxMessages.length > 0) {
|
|
368
|
+
for (const msg of inboxMessages) {
|
|
369
|
+
console.log(
|
|
370
|
+
` from: ${msg.payload.from}, to: ${JSON.stringify(msg.payload.to)}, ` +
|
|
371
|
+
`thread: ${msg.payload.threadTag || "none"}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Verify overall session succeeded ──
|
|
377
|
+
const result = getResult(run.messages);
|
|
378
|
+
console.log(`[tier6] result: ${result?.subtype || "success"}, cost: $${result?.total_cost_usd?.toFixed(2) || "?"}`);
|
|
379
|
+
expect(result?.is_error).toBeFalsy();
|
|
380
|
+
|
|
381
|
+
// ── Verify MAP received lifecycle events ──
|
|
382
|
+
expect(mockServer.getByMethod("map/connect").length).toBeGreaterThan(0);
|
|
383
|
+
expect(mockServer.getByMethod("map/agents/register").length).toBeGreaterThan(0);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("inbox storage has agent registrations after /swarm run", async () => {
|
|
387
|
+
// This test runs AFTER the team flow test above, checking persistent state.
|
|
388
|
+
// Find the inbox socket from the workspace's sidecar.
|
|
389
|
+
const mapDir = path.join(
|
|
390
|
+
workspace?.dir || "/nonexistent",
|
|
391
|
+
".swarm", "claude-swarm", "tmp", "map"
|
|
392
|
+
);
|
|
393
|
+
const inboxSockPath = path.join(mapDir, "inbox.sock");
|
|
394
|
+
|
|
395
|
+
if (!workspace || !fs.existsSync(inboxSockPath)) {
|
|
396
|
+
console.log("[tier6] skipping: inbox socket not found (sidecar may have exited)");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Query the inbox for registered agents
|
|
401
|
+
const listResp = await sendCommand(inboxSockPath, {
|
|
402
|
+
action: "list_agents",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (listResp?.ok) {
|
|
406
|
+
console.log(`[tier6] inbox agents: ${listResp.count}`);
|
|
407
|
+
for (const agent of (listResp.agents || [])) {
|
|
408
|
+
console.log(` ${agent.agentId} — status: ${agent.status}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// After a /swarm run, at least the sidecar-spawned agents should be registered
|
|
412
|
+
// (they may be "disconnected" if the session ended)
|
|
413
|
+
if (listResp.count > 0) {
|
|
414
|
+
expect(listResp.agents.length).toBeGreaterThan(0);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
console.log("[tier6] list_agents not supported or sidecar exited");
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
424
|
+
// Group 4: Threaded conversation — two sequential agents use same threadTag
|
|
425
|
+
//
|
|
426
|
+
// Verifies that threaded inbox conversations work across agent turns.
|
|
427
|
+
// Agent A sends a message with a threadTag, Agent B reads the thread.
|
|
428
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
describe.skipIf(!LIVE || !CLI_AVAILABLE || !agentInboxAvailable)(
|
|
431
|
+
"tier6: threaded inbox conversation across agent turns",
|
|
432
|
+
{ timeout: 300_000 },
|
|
433
|
+
() => {
|
|
434
|
+
let mockServer;
|
|
435
|
+
let workspace;
|
|
436
|
+
let sidecar;
|
|
437
|
+
|
|
438
|
+
afterAll(async () => {
|
|
439
|
+
if (sidecar) sidecar.cleanup();
|
|
440
|
+
if (workspace) {
|
|
441
|
+
cleanupWorkspace(workspace.dir);
|
|
442
|
+
workspace.cleanup();
|
|
443
|
+
}
|
|
444
|
+
if (mockServer) await mockServer.stop();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("sequential agents build a thread via inbox send_message + read_thread", async () => {
|
|
448
|
+
mockServer = new MockMapServer();
|
|
449
|
+
await mockServer.start();
|
|
450
|
+
|
|
451
|
+
workspace = createWorkspace({
|
|
452
|
+
tmpdir: SHORT_TMPDIR, prefix: "s6-thread-",
|
|
453
|
+
config: {
|
|
454
|
+
template: "gsd",
|
|
455
|
+
map: {
|
|
456
|
+
enabled: true,
|
|
457
|
+
server: `ws://localhost:${mockServer.port}`,
|
|
458
|
+
sidecar: "session",
|
|
459
|
+
},
|
|
460
|
+
inbox: { enabled: true },
|
|
461
|
+
},
|
|
462
|
+
files: {
|
|
463
|
+
"README.md": "# Thread Test\n",
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Start sidecar with inbox
|
|
468
|
+
sidecar = await startTestSidecar({
|
|
469
|
+
workspaceDir: workspace.dir,
|
|
470
|
+
mockServerPort: mockServer.port,
|
|
471
|
+
inboxConfig: { enabled: true },
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(sidecar.inboxReady).toBe(true);
|
|
475
|
+
|
|
476
|
+
// Session 1: Agent sends initial thread message
|
|
477
|
+
const run1 = await runClaude(
|
|
478
|
+
'Use the agent-inbox send_message MCP tool to send a message:\n' +
|
|
479
|
+
'- to: "gsd-reviewer"\n' +
|
|
480
|
+
'- body: "Code review requested for PR #42"\n' +
|
|
481
|
+
'- threadTag: "pr-42-review"\n' +
|
|
482
|
+
'- from: "gsd-developer"\n' +
|
|
483
|
+
'Then confirm the send was successful.',
|
|
484
|
+
{
|
|
485
|
+
cwd: workspace.dir,
|
|
486
|
+
maxBudgetUsd: 1.0,
|
|
487
|
+
maxTurns: 5,
|
|
488
|
+
timeout: 60_000,
|
|
489
|
+
label: "tier6-thread-send",
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const sendCalls1 = extractToolCalls(run1.messages).filter((tc) =>
|
|
494
|
+
tc.name.includes("send_message")
|
|
495
|
+
);
|
|
496
|
+
console.log(`[tier6] session 1 send_message calls: ${sendCalls1.length}`);
|
|
497
|
+
expect(sendCalls1.length).toBeGreaterThanOrEqual(1);
|
|
498
|
+
|
|
499
|
+
// Verify message is in real storage
|
|
500
|
+
const checkResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
501
|
+
action: "check_inbox",
|
|
502
|
+
agentId: "gsd-reviewer",
|
|
503
|
+
});
|
|
504
|
+
console.log("[tier6] reviewer inbox after session 1:", JSON.stringify(checkResp));
|
|
505
|
+
|
|
506
|
+
// Session 2: Agent reads the thread and adds a reply
|
|
507
|
+
const run2 = await runClaude(
|
|
508
|
+
'Use the agent-inbox MCP tools to:\n' +
|
|
509
|
+
'1. Use read_thread with threadTag "pr-42-review" to see the conversation\n' +
|
|
510
|
+
'2. Use send_message to reply:\n' +
|
|
511
|
+
' - to: "gsd-developer"\n' +
|
|
512
|
+
' - body: "LGTM, approved"\n' +
|
|
513
|
+
' - threadTag: "pr-42-review"\n' +
|
|
514
|
+
' - from: "gsd-reviewer"\n' +
|
|
515
|
+
'3. Use read_thread again to verify both messages are in the thread\n' +
|
|
516
|
+
'Report what the thread looks like.',
|
|
517
|
+
{
|
|
518
|
+
cwd: workspace.dir,
|
|
519
|
+
maxBudgetUsd: 1.0,
|
|
520
|
+
maxTurns: 10,
|
|
521
|
+
timeout: 60_000,
|
|
522
|
+
label: "tier6-thread-reply",
|
|
523
|
+
}
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const toolCalls2 = extractToolCalls(run2.messages);
|
|
527
|
+
const readCalls = toolCalls2.filter((tc) => tc.name.includes("read_thread"));
|
|
528
|
+
const sendCalls2 = toolCalls2.filter((tc) => tc.name.includes("send_message"));
|
|
529
|
+
|
|
530
|
+
console.log(`[tier6] session 2 — read_thread: ${readCalls.length}, send_message: ${sendCalls2.length}`);
|
|
531
|
+
console.log(`[tier6] session 2 all tools: ${toolCalls2.map((tc) => tc.name).join(", ")}`);
|
|
532
|
+
|
|
533
|
+
expect(readCalls.length).toBeGreaterThanOrEqual(1);
|
|
534
|
+
expect(sendCalls2.length).toBeGreaterThanOrEqual(1);
|
|
535
|
+
|
|
536
|
+
// Verify the thread has both messages via IPC
|
|
537
|
+
const threadResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
538
|
+
action: "read_thread",
|
|
539
|
+
threadTag: "pr-42-review",
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
console.log("[tier6] thread after both sessions:", JSON.stringify(threadResp));
|
|
543
|
+
|
|
544
|
+
if (threadResp?.ok) {
|
|
545
|
+
expect(threadResp.count).toBeGreaterThanOrEqual(2);
|
|
546
|
+
console.log(`[tier6] thread message count: ${threadResp.count}`);
|
|
547
|
+
|
|
548
|
+
// Verify both senders appear
|
|
549
|
+
const senders = (threadResp.messages || []).map((m) => m.sender_id);
|
|
550
|
+
console.log(`[tier6] thread senders: ${senders.join(", ")}`);
|
|
551
|
+
|
|
552
|
+
const hasDeveloper = senders.some((s) => s.includes("developer"));
|
|
553
|
+
const hasReviewer = senders.some((s) => s.includes("reviewer"));
|
|
554
|
+
console.log(`[tier6] developer in thread: ${hasDeveloper}, reviewer: ${hasReviewer}`);
|
|
555
|
+
|
|
556
|
+
expect(hasDeveloper || hasReviewer).toBe(true);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Verify no errors
|
|
560
|
+
expect(getResult(run2.messages)?.is_error).toBeFalsy();
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
566
|
+
// Group 5: Two spawned agents messaging each other via inbox
|
|
567
|
+
//
|
|
568
|
+
// The coordinator creates a team with two agents. Agent A (writer) sends
|
|
569
|
+
// an inbox message to Agent B (reviewer) with a specific threadTag.
|
|
570
|
+
// Agent B checks its inbox, reads the message, and replies via inbox.
|
|
571
|
+
// After the session, we verify via the real inbox IPC socket that:
|
|
572
|
+
// - Both agents sent messages
|
|
573
|
+
// - The thread contains messages from both agents
|
|
574
|
+
// - Messages have correct sender/recipient
|
|
575
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
describe.skipIf(!LIVE || !CLI_AVAILABLE || !agentInboxAvailable)(
|
|
578
|
+
"tier6: two spawned agents messaging each other via inbox",
|
|
579
|
+
{ timeout: 600_000 },
|
|
580
|
+
() => {
|
|
581
|
+
let mockServer;
|
|
582
|
+
let workspace;
|
|
583
|
+
let sidecar;
|
|
584
|
+
|
|
585
|
+
afterAll(async () => {
|
|
586
|
+
if (sidecar) sidecar.cleanup();
|
|
587
|
+
if (workspace) {
|
|
588
|
+
cleanupWorkspace(workspace.dir);
|
|
589
|
+
workspace.cleanup();
|
|
590
|
+
}
|
|
591
|
+
if (mockServer) await mockServer.stop();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("two team agents exchange messages via inbox MCP tools", async () => {
|
|
595
|
+
mockServer = new MockMapServer();
|
|
596
|
+
await mockServer.start();
|
|
597
|
+
|
|
598
|
+
workspace = createWorkspace({
|
|
599
|
+
tmpdir: SHORT_TMPDIR, prefix: "s6-2agent-",
|
|
600
|
+
config: {
|
|
601
|
+
template: "gsd",
|
|
602
|
+
map: {
|
|
603
|
+
enabled: true,
|
|
604
|
+
server: `ws://localhost:${mockServer.port}`,
|
|
605
|
+
sidecar: "session",
|
|
606
|
+
},
|
|
607
|
+
inbox: { enabled: true },
|
|
608
|
+
},
|
|
609
|
+
files: {
|
|
610
|
+
"README.md": "# Two-Agent Inbox Test\n",
|
|
611
|
+
"spec.txt": "Feature: user login page\n",
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Start sidecar with inbox BEFORE running the agent
|
|
616
|
+
sidecar = await startTestSidecar({
|
|
617
|
+
workspaceDir: workspace.dir,
|
|
618
|
+
mockServerPort: mockServer.port,
|
|
619
|
+
inboxConfig: { enabled: true },
|
|
620
|
+
});
|
|
621
|
+
expect(sidecar.inboxReady).toBe(true);
|
|
622
|
+
|
|
623
|
+
// The prompt creates a team with exactly two agents who MUST message each
|
|
624
|
+
// other via inbox. The agents are given very explicit, deterministic
|
|
625
|
+
// instructions to minimize LLM non-determinism.
|
|
626
|
+
const run = await runClaude(
|
|
627
|
+
`You are a coordinator. Do the following steps IN ORDER:
|
|
628
|
+
|
|
629
|
+
1. Create a team: TeamCreate(team_name="inbox-test-team", description="Inbox messaging test")
|
|
630
|
+
|
|
631
|
+
2. Spawn Agent A (the writer):
|
|
632
|
+
Agent(
|
|
633
|
+
name="writer",
|
|
634
|
+
team_name="inbox-test-team",
|
|
635
|
+
prompt="You are the writer agent. Your ONLY job is to use the agent-inbox MCP tools. Do these steps exactly:
|
|
636
|
+
Step 1: Use agent-inbox send_message to send a message with these EXACT parameters:
|
|
637
|
+
- to: inbox-test-reviewer
|
|
638
|
+
- body: WRITER_MSG_001 Please review the login page spec
|
|
639
|
+
- from: inbox-test-writer
|
|
640
|
+
- threadTag: login-review-thread
|
|
641
|
+
Step 2: Wait a moment, then use agent-inbox check_inbox with agentId: inbox-test-writer to see if you got a reply.
|
|
642
|
+
Step 3: Report what happened."
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
3. Spawn Agent B (the reviewer):
|
|
646
|
+
Agent(
|
|
647
|
+
name="reviewer",
|
|
648
|
+
team_name="inbox-test-team",
|
|
649
|
+
prompt="You are the reviewer agent. Your ONLY job is to use the agent-inbox MCP tools. Do these steps exactly:
|
|
650
|
+
Step 1: Use agent-inbox check_inbox with agentId: inbox-test-reviewer to check for messages.
|
|
651
|
+
Step 2: Use agent-inbox read_thread with threadTag: login-review-thread to see the full thread.
|
|
652
|
+
Step 3: Use agent-inbox send_message to reply with these EXACT parameters:
|
|
653
|
+
- to: inbox-test-writer
|
|
654
|
+
- body: REVIEWER_MSG_001 Looks good, approved with minor comments
|
|
655
|
+
- from: inbox-test-reviewer
|
|
656
|
+
- threadTag: login-review-thread
|
|
657
|
+
Step 4: Report what happened."
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
4. After both agents finish, report their results.
|
|
661
|
+
|
|
662
|
+
IMPORTANT: You MUST spawn both agents. Do NOT do their work yourself.`,
|
|
663
|
+
{
|
|
664
|
+
cwd: workspace.dir,
|
|
665
|
+
maxBudgetUsd: 10.0,
|
|
666
|
+
maxTurns: 30,
|
|
667
|
+
timeout: 300_000,
|
|
668
|
+
label: "tier6-two-agent-inbox",
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const toolCalls = extractToolCalls(run.messages);
|
|
673
|
+
const toolNames = toolCalls.map((tc) => tc.name);
|
|
674
|
+
console.log("[tier6] 2-agent tool calls:", toolNames.join(", "));
|
|
675
|
+
|
|
676
|
+
// ── Verify team was created and agents were spawned ──
|
|
677
|
+
const teamCreates = findToolCalls(run.messages, "TeamCreate");
|
|
678
|
+
const agentCalls = findToolCalls(run.messages, "Agent");
|
|
679
|
+
|
|
680
|
+
console.log(`[tier6] TeamCreate: ${teamCreates.length}, Agent: ${agentCalls.length}`);
|
|
681
|
+
expect(teamCreates.length).toBeGreaterThanOrEqual(1);
|
|
682
|
+
expect(agentCalls.length).toBeGreaterThanOrEqual(2);
|
|
683
|
+
|
|
684
|
+
// Allow time for all async operations to settle
|
|
685
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
686
|
+
|
|
687
|
+
// ── Verify messages in real inbox storage via IPC ──
|
|
688
|
+
|
|
689
|
+
// Check writer's inbox (should have reviewer's reply)
|
|
690
|
+
const writerInbox = await sendCommand(sidecar.inboxSocketPath, {
|
|
691
|
+
action: "check_inbox",
|
|
692
|
+
agentId: "inbox-test-writer",
|
|
693
|
+
});
|
|
694
|
+
console.log("[tier6] writer inbox:", JSON.stringify(writerInbox));
|
|
695
|
+
|
|
696
|
+
// Check reviewer's inbox (should have writer's initial message)
|
|
697
|
+
const reviewerInbox = await sendCommand(sidecar.inboxSocketPath, {
|
|
698
|
+
action: "check_inbox",
|
|
699
|
+
agentId: "inbox-test-reviewer",
|
|
700
|
+
});
|
|
701
|
+
console.log("[tier6] reviewer inbox:", JSON.stringify(reviewerInbox));
|
|
702
|
+
|
|
703
|
+
// Check the shared thread
|
|
704
|
+
const threadResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
705
|
+
action: "read_thread",
|
|
706
|
+
threadTag: "login-review-thread",
|
|
707
|
+
});
|
|
708
|
+
console.log("[tier6] thread:", JSON.stringify(threadResp));
|
|
709
|
+
|
|
710
|
+
// ── Assertions ──
|
|
711
|
+
|
|
712
|
+
// At least one message should have been exchanged via inbox.
|
|
713
|
+
// Due to LLM non-determinism, we check multiple signals:
|
|
714
|
+
const writerSent = reviewerInbox?.ok && reviewerInbox?.messages?.length > 0;
|
|
715
|
+
const reviewerReplied = writerInbox?.ok && writerInbox?.messages?.length > 0;
|
|
716
|
+
const threadExists = threadResp?.ok && threadResp?.count > 0;
|
|
717
|
+
|
|
718
|
+
console.log(
|
|
719
|
+
`[tier6] writer→reviewer: ${writerSent}, reviewer→writer: ${reviewerReplied}, thread: ${threadExists}`
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// The agent output may also contain evidence of inbox tool usage
|
|
723
|
+
const allText = run.stdout + run.stderr;
|
|
724
|
+
const mentionsWriterMsg = allText.includes("WRITER_MSG_001");
|
|
725
|
+
const mentionsReviewerMsg = allText.includes("REVIEWER_MSG_001");
|
|
726
|
+
console.log(`[tier6] output mentions writer msg: ${mentionsWriterMsg}, reviewer msg: ${mentionsReviewerMsg}`);
|
|
727
|
+
|
|
728
|
+
// Primary assertion: at least one direction of messaging worked
|
|
729
|
+
const messagingWorked = writerSent || reviewerReplied || threadExists || mentionsWriterMsg || mentionsReviewerMsg;
|
|
730
|
+
expect(messagingWorked).toBe(true);
|
|
731
|
+
|
|
732
|
+
// If the thread exists, verify it has messages from both sides
|
|
733
|
+
if (threadExists && threadResp.count >= 2) {
|
|
734
|
+
const senders = threadResp.messages.map((m) => m.sender_id);
|
|
735
|
+
console.log(`[tier6] thread senders: ${senders.join(", ")}`);
|
|
736
|
+
|
|
737
|
+
const hasWriter = senders.some((s) => s.includes("writer"));
|
|
738
|
+
const hasReviewer = senders.some((s) => s.includes("reviewer"));
|
|
739
|
+
console.log(`[tier6] writer in thread: ${hasWriter}, reviewer: ${hasReviewer}`);
|
|
740
|
+
|
|
741
|
+
// Both agents should appear in the thread
|
|
742
|
+
expect(hasWriter && hasReviewer).toBe(true);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Verify session completed without error
|
|
746
|
+
const result = getResult(run.messages);
|
|
747
|
+
console.log(`[tier6] result: ${result?.subtype || "success"}, cost: $${result?.total_cost_usd?.toFixed(2) || "?"}`);
|
|
748
|
+
expect(result?.is_error).toBeFalsy();
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("inbox thread persists after agents complete and is queryable", async () => {
|
|
752
|
+
// This test runs after the previous one, verifying the inbox state persists.
|
|
753
|
+
if (!sidecar?.inboxSocketPath || !fs.existsSync(sidecar.inboxSocketPath)) {
|
|
754
|
+
console.log("[tier6] skipping: inbox socket gone (sidecar may have exited)");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// The thread should still be readable even after agents finished
|
|
759
|
+
const threadResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
760
|
+
action: "read_thread",
|
|
761
|
+
threadTag: "login-review-thread",
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
if (threadResp?.ok && threadResp.count > 0) {
|
|
765
|
+
console.log(`[tier6] persistent thread has ${threadResp.count} messages`);
|
|
766
|
+
expect(threadResp.count).toBeGreaterThanOrEqual(1);
|
|
767
|
+
|
|
768
|
+
// Messages should have real content, not be empty
|
|
769
|
+
for (const msg of threadResp.messages) {
|
|
770
|
+
expect(msg.sender_id).toBeTruthy();
|
|
771
|
+
console.log(` [${msg.sender_id}]: ${JSON.stringify(msg.content).slice(0, 80)}`);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
console.log("[tier6] thread not found — agents may not have used threadTag");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// List all agents that were registered during the session
|
|
778
|
+
const listResp = await sendCommand(sidecar.inboxSocketPath, {
|
|
779
|
+
action: "list_agents",
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
if (listResp?.ok) {
|
|
783
|
+
console.log(`[tier6] registered agents: ${listResp.count}`);
|
|
784
|
+
for (const agent of (listResp.agents || [])) {
|
|
785
|
+
console.log(` ${agent.agentId} — ${agent.status}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("MAP server received inbox.message bridge events for agent-to-agent messages", async () => {
|
|
791
|
+
// Check if the MAP mock server received inbox.message events
|
|
792
|
+
// from the message.created bridge in map-sidecar.mjs
|
|
793
|
+
const inboxMessages = mockServer.sentMessages.filter(
|
|
794
|
+
(m) => m.payload?.type === "inbox.message"
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
console.log(`[tier6] inbox.message MAP events: ${inboxMessages.length}`);
|
|
798
|
+
for (const msg of inboxMessages) {
|
|
799
|
+
console.log(
|
|
800
|
+
` from: ${msg.payload.from}, to: ${JSON.stringify(msg.payload.to)}, ` +
|
|
801
|
+
`thread: ${msg.payload.threadTag || "none"}`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (inboxMessages.length > 0) {
|
|
806
|
+
// Verify the bridge events have correct structure
|
|
807
|
+
for (const msg of inboxMessages) {
|
|
808
|
+
expect(msg.payload.messageId).toBeTruthy();
|
|
809
|
+
expect(msg.payload.from).toBeTruthy();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Look for messages in the login-review-thread
|
|
813
|
+
const threadMessages = inboxMessages.filter(
|
|
814
|
+
(m) => m.payload.threadTag === "login-review-thread"
|
|
815
|
+
);
|
|
816
|
+
if (threadMessages.length >= 2) {
|
|
817
|
+
const bridgeSenders = threadMessages.map((m) => m.payload.from);
|
|
818
|
+
console.log(`[tier6] bridge thread senders: ${bridgeSenders.join(", ")}`);
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
console.log(
|
|
822
|
+
"[tier6] NOTE: No inbox.message bridge events — " +
|
|
823
|
+
"sidecar may not wire message.created → MAP in test mode"
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
831
|
+
// Group 6: MAP bridge — inbox.message events appear on MAP server
|
|
832
|
+
//
|
|
833
|
+
// Verifies that when agents send messages via inbox, the message.created
|
|
834
|
+
// event bridge emits inbox.message events to the MAP server.
|
|
835
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
describe.skipIf(!LIVE || !CLI_AVAILABLE || !agentInboxAvailable)(
|
|
838
|
+
"tier6: inbox message.created → MAP inbox.message bridge",
|
|
839
|
+
{ timeout: 300_000 },
|
|
840
|
+
() => {
|
|
841
|
+
let mockServer;
|
|
842
|
+
let workspace;
|
|
843
|
+
let sidecar;
|
|
844
|
+
|
|
845
|
+
afterAll(async () => {
|
|
846
|
+
if (sidecar) sidecar.cleanup();
|
|
847
|
+
if (workspace) {
|
|
848
|
+
cleanupWorkspace(workspace.dir);
|
|
849
|
+
workspace.cleanup();
|
|
850
|
+
}
|
|
851
|
+
if (mockServer) await mockServer.stop();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it("inbox send triggers inbox.message event on MAP server", async () => {
|
|
855
|
+
mockServer = new MockMapServer();
|
|
856
|
+
await mockServer.start();
|
|
857
|
+
|
|
858
|
+
workspace = createWorkspace({
|
|
859
|
+
tmpdir: SHORT_TMPDIR, prefix: "s6-bridge-",
|
|
860
|
+
config: {
|
|
861
|
+
template: "gsd",
|
|
862
|
+
map: {
|
|
863
|
+
enabled: true,
|
|
864
|
+
server: `ws://localhost:${mockServer.port}`,
|
|
865
|
+
sidecar: "session",
|
|
866
|
+
},
|
|
867
|
+
inbox: { enabled: true },
|
|
868
|
+
},
|
|
869
|
+
files: {
|
|
870
|
+
"README.md": "# Bridge Test\n",
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
sidecar = await startTestSidecar({
|
|
875
|
+
workspaceDir: workspace.dir,
|
|
876
|
+
mockServerPort: mockServer.port,
|
|
877
|
+
inboxConfig: { enabled: true },
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
expect(sidecar.inboxReady).toBe(true);
|
|
881
|
+
|
|
882
|
+
// Send messages directly via inbox IPC (bypassing MCP, testing the bridge)
|
|
883
|
+
await sendCommand(sidecar.inboxSocketPath, {
|
|
884
|
+
action: "send",
|
|
885
|
+
from: "gsd-lead",
|
|
886
|
+
to: "gsd-executor",
|
|
887
|
+
payload: "Start implementation of feature Y",
|
|
888
|
+
threadTag: "feature-y",
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
await sendCommand(sidecar.inboxSocketPath, {
|
|
892
|
+
action: "send",
|
|
893
|
+
from: "gsd-executor",
|
|
894
|
+
to: "gsd-lead",
|
|
895
|
+
payload: "Feature Y implemented",
|
|
896
|
+
threadTag: "feature-y",
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// Wait for message.created events to be bridged to MAP
|
|
900
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
901
|
+
|
|
902
|
+
// Check MAP server for inbox.message events
|
|
903
|
+
const inboxMessages = mockServer.sentMessages.filter(
|
|
904
|
+
(m) => m.payload?.type === "inbox.message"
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
console.log(`[tier6] inbox.message MAP events: ${inboxMessages.length}`);
|
|
908
|
+
for (const msg of inboxMessages) {
|
|
909
|
+
console.log(
|
|
910
|
+
` from: ${msg.payload.from}, to: ${JSON.stringify(msg.payload.to)}, thread: ${msg.payload.threadTag}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// The bridge should have emitted inbox.message events for each send
|
|
915
|
+
// NOTE: This only works if the sidecar subscribed to message.created events
|
|
916
|
+
// (which happens in map-sidecar.mjs when inboxInstance?.events && connection)
|
|
917
|
+
if (inboxMessages.length > 0) {
|
|
918
|
+
expect(inboxMessages.length).toBeGreaterThanOrEqual(2);
|
|
919
|
+
|
|
920
|
+
const firstMsg = inboxMessages[0].payload;
|
|
921
|
+
expect(firstMsg.from).toBe("gsd-lead");
|
|
922
|
+
expect(firstMsg.threadTag).toBe("feature-y");
|
|
923
|
+
|
|
924
|
+
const secondMsg = inboxMessages[1].payload;
|
|
925
|
+
expect(secondMsg.from).toBe("gsd-executor");
|
|
926
|
+
expect(secondMsg.threadTag).toBe("feature-y");
|
|
927
|
+
} else {
|
|
928
|
+
// If no bridge events, this is expected when the sidecar uses the
|
|
929
|
+
// test helper (startTestSidecar) which may not wire up the event bridge.
|
|
930
|
+
// Log for debugging.
|
|
931
|
+
console.log(
|
|
932
|
+
"[tier6] NOTE: No inbox.message bridge events. " +
|
|
933
|
+
"This is expected if the sidecar process doesn't wire message.created → MAP."
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
);
|