claude-code-swarm 0.3.5 → 0.3.7
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 +6 -2
- package/references/agent-inbox/package-lock.json +2 -2
- package/references/agent-inbox/package.json +1 -1
- 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/references/minimem/package-lock.json +2 -2
- package/references/minimem/package.json +1 -1
- 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,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 7: OpenTasks Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the opentasks IPC client and MAP bridge without LLM calls:
|
|
5
|
+
* 1. rpcRequest round-trip via a test daemon
|
|
6
|
+
* 2. createTask / updateTask JSON-RPC round-trip
|
|
7
|
+
* 3. pushSyncEvent for various event types
|
|
8
|
+
* 4. findSocketPath discovery priority
|
|
9
|
+
* 5. isDaemonAlive with live and dead sockets
|
|
10
|
+
* 6. MAP bridge events for task lifecycle (sidecar + mock MAP server)
|
|
11
|
+
*
|
|
12
|
+
* Uses a minimal test daemon (e2e/helpers/opentasks-daemon.mjs) for IPC tests.
|
|
13
|
+
* No LLM calls.
|
|
14
|
+
*
|
|
15
|
+
* Run:
|
|
16
|
+
* npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier7-opentasks.test.mjs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect, afterEach, beforeAll, afterAll } from "vitest";
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
import { createWorkspace } from "./helpers/workspace.mjs";
|
|
24
|
+
import { startTestDaemon } from "./helpers/opentasks-daemon.mjs";
|
|
25
|
+
import { MockMapServer } from "./helpers/map-mock-server.mjs";
|
|
26
|
+
import { startTestSidecar, sendCommand } from "./helpers/sidecar.mjs";
|
|
27
|
+
import { waitFor } from "./helpers/cleanup.mjs";
|
|
28
|
+
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const SHORT_TMPDIR = "/tmp";
|
|
31
|
+
|
|
32
|
+
// Import opentasks client functions
|
|
33
|
+
const {
|
|
34
|
+
findSocketPath,
|
|
35
|
+
rpcRequest,
|
|
36
|
+
isDaemonAlive,
|
|
37
|
+
createTask,
|
|
38
|
+
updateTask,
|
|
39
|
+
pushSyncEvent,
|
|
40
|
+
} = await import("../src/opentasks-client.mjs");
|
|
41
|
+
|
|
42
|
+
const { buildCapabilitiesContext } = await import("../src/context-output.mjs");
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Group 1: findSocketPath — socket discovery priority
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe(
|
|
49
|
+
"tier7: opentasks findSocketPath",
|
|
50
|
+
{ timeout: 15_000 },
|
|
51
|
+
() => {
|
|
52
|
+
let workspace;
|
|
53
|
+
let origCwd;
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
if (origCwd) process.chdir(origCwd);
|
|
57
|
+
if (workspace) {
|
|
58
|
+
workspace.cleanup();
|
|
59
|
+
workspace = null;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("prefers .swarm/opentasks/ layout", () => {
|
|
64
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-" });
|
|
65
|
+
origCwd = process.cwd();
|
|
66
|
+
process.chdir(workspace.dir);
|
|
67
|
+
|
|
68
|
+
const swarmSock = path.join(workspace.dir, ".swarm", "opentasks", "daemon.sock");
|
|
69
|
+
fs.mkdirSync(path.dirname(swarmSock), { recursive: true });
|
|
70
|
+
fs.writeFileSync(swarmSock, "");
|
|
71
|
+
|
|
72
|
+
expect(findSocketPath()).toBe(path.join(".swarm", "opentasks", "daemon.sock"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("falls back to .opentasks/ layout", () => {
|
|
76
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-" });
|
|
77
|
+
origCwd = process.cwd();
|
|
78
|
+
process.chdir(workspace.dir);
|
|
79
|
+
|
|
80
|
+
const otSock = path.join(workspace.dir, ".opentasks", "daemon.sock");
|
|
81
|
+
fs.mkdirSync(path.dirname(otSock), { recursive: true });
|
|
82
|
+
fs.writeFileSync(otSock, "");
|
|
83
|
+
|
|
84
|
+
expect(findSocketPath()).toBe(path.join(".opentasks", "daemon.sock"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("falls back to .git/opentasks/ layout", () => {
|
|
88
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-" });
|
|
89
|
+
origCwd = process.cwd();
|
|
90
|
+
process.chdir(workspace.dir);
|
|
91
|
+
|
|
92
|
+
const gitSock = path.join(workspace.dir, ".git", "opentasks", "daemon.sock");
|
|
93
|
+
fs.mkdirSync(path.dirname(gitSock), { recursive: true });
|
|
94
|
+
fs.writeFileSync(gitSock, "");
|
|
95
|
+
|
|
96
|
+
expect(findSocketPath()).toBe(path.join(".git", "opentasks", "daemon.sock"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns default swarmkit path when no socket exists", () => {
|
|
100
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-" });
|
|
101
|
+
origCwd = process.cwd();
|
|
102
|
+
process.chdir(workspace.dir);
|
|
103
|
+
|
|
104
|
+
expect(findSocketPath()).toBe(path.join(".swarm", "opentasks", "daemon.sock"));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
// Group 2: Daemon IPC — rpcRequest, isDaemonAlive with test daemon
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
describe(
|
|
114
|
+
"tier7: opentasks daemon IPC",
|
|
115
|
+
{ timeout: 30_000 },
|
|
116
|
+
() => {
|
|
117
|
+
let daemon;
|
|
118
|
+
let workspace;
|
|
119
|
+
|
|
120
|
+
beforeAll(async () => {
|
|
121
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-ipc-" });
|
|
122
|
+
const sockPath = path.join(workspace.dir, "daemon.sock");
|
|
123
|
+
daemon = await startTestDaemon(sockPath);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterAll(async () => {
|
|
127
|
+
if (daemon) await daemon.stop();
|
|
128
|
+
if (workspace) workspace.cleanup();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("rpcRequest ping returns result", async () => {
|
|
132
|
+
const result = await rpcRequest("ping", {}, daemon.socketPath);
|
|
133
|
+
expect(result).not.toBeNull();
|
|
134
|
+
expect(result.pong).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("isDaemonAlive returns true for running daemon", async () => {
|
|
138
|
+
const alive = await isDaemonAlive(daemon.socketPath);
|
|
139
|
+
expect(alive).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("isDaemonAlive returns false for non-existent socket", async () => {
|
|
143
|
+
const alive = await isDaemonAlive("/tmp/nonexistent-" + Date.now() + ".sock");
|
|
144
|
+
expect(alive).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("rpcRequest with unknown method returns null", async () => {
|
|
148
|
+
const result = await rpcRequest("nonexistent.method", {}, daemon.socketPath);
|
|
149
|
+
expect(result).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("rpcRequest with dead socket returns null (never throws)", async () => {
|
|
153
|
+
const result = await rpcRequest("ping", {}, "/tmp/no-daemon-" + Date.now() + ".sock");
|
|
154
|
+
expect(result).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
// Group 3: Task CRUD — createTask, updateTask round-trip
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
describe(
|
|
164
|
+
"tier7: opentasks task CRUD",
|
|
165
|
+
{ timeout: 30_000 },
|
|
166
|
+
() => {
|
|
167
|
+
let daemon;
|
|
168
|
+
let workspace;
|
|
169
|
+
|
|
170
|
+
beforeAll(async () => {
|
|
171
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-crud-" });
|
|
172
|
+
const sockPath = path.join(workspace.dir, "daemon.sock");
|
|
173
|
+
daemon = await startTestDaemon(sockPath);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
afterAll(async () => {
|
|
177
|
+
if (daemon) await daemon.stop();
|
|
178
|
+
if (workspace) workspace.cleanup();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("createTask returns a node with an ID", async () => {
|
|
182
|
+
const result = await createTask(daemon.socketPath, {
|
|
183
|
+
title: "Test task from tier7",
|
|
184
|
+
status: "open",
|
|
185
|
+
assignee: "test-agent",
|
|
186
|
+
metadata: { source: "e2e-test" },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result).not.toBeNull();
|
|
190
|
+
expect(result.id).toBeTruthy();
|
|
191
|
+
expect(result.title).toBe("Test task from tier7");
|
|
192
|
+
expect(result.status).toBe("open");
|
|
193
|
+
expect(result.assignee).toBe("test-agent");
|
|
194
|
+
|
|
195
|
+
// Verify it's in the daemon's storage
|
|
196
|
+
expect(daemon.nodes.has(result.id)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("updateTask changes task status", async () => {
|
|
200
|
+
const created = await createTask(daemon.socketPath, {
|
|
201
|
+
title: "Task to update",
|
|
202
|
+
status: "open",
|
|
203
|
+
});
|
|
204
|
+
expect(created).not.toBeNull();
|
|
205
|
+
|
|
206
|
+
const updated = await updateTask(daemon.socketPath, created.id, {
|
|
207
|
+
status: "in_progress",
|
|
208
|
+
assignee: "gsd-executor",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(updated).not.toBeNull();
|
|
212
|
+
expect(updated.status).toBe("in_progress");
|
|
213
|
+
expect(updated.assignee).toBe("gsd-executor");
|
|
214
|
+
|
|
215
|
+
// Verify in daemon storage
|
|
216
|
+
const stored = daemon.nodes.get(created.id);
|
|
217
|
+
expect(stored.status).toBe("in_progress");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("updateTask with non-existent ID returns null", async () => {
|
|
221
|
+
const result = await updateTask(daemon.socketPath, "nonexistent-id", {
|
|
222
|
+
status: "done",
|
|
223
|
+
});
|
|
224
|
+
expect(result).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("createTask with missing socket returns null (never throws)", async () => {
|
|
228
|
+
const result = await createTask("/tmp/no-daemon-" + Date.now() + ".sock", {
|
|
229
|
+
title: "Should fail gracefully",
|
|
230
|
+
status: "open",
|
|
231
|
+
});
|
|
232
|
+
expect(result).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// Group 4: pushSyncEvent — forwarding MAP task events to graph
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe(
|
|
242
|
+
"tier7: opentasks pushSyncEvent",
|
|
243
|
+
{ timeout: 30_000 },
|
|
244
|
+
() => {
|
|
245
|
+
let daemon;
|
|
246
|
+
let workspace;
|
|
247
|
+
|
|
248
|
+
beforeAll(async () => {
|
|
249
|
+
workspace = createWorkspace({ tmpdir: SHORT_TMPDIR, prefix: "t7-ot-sync-" });
|
|
250
|
+
const sockPath = path.join(workspace.dir, "daemon.sock");
|
|
251
|
+
daemon = await startTestDaemon(sockPath);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
afterAll(async () => {
|
|
255
|
+
if (daemon) await daemon.stop();
|
|
256
|
+
if (workspace) workspace.cleanup();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("task.sync creates a new node in the graph", async () => {
|
|
260
|
+
const ok = await pushSyncEvent(daemon.socketPath, {
|
|
261
|
+
type: "task.sync",
|
|
262
|
+
uri: "map://remote-system/task-1",
|
|
263
|
+
subject: "Remote task synced from MAP",
|
|
264
|
+
status: "open",
|
|
265
|
+
source: "map-bridge",
|
|
266
|
+
});
|
|
267
|
+
expect(ok).toBe(true);
|
|
268
|
+
|
|
269
|
+
// Should have created a node
|
|
270
|
+
const nodes = Array.from(daemon.nodes.values());
|
|
271
|
+
const synced = nodes.find((n) => n.uri === "map://remote-system/task-1");
|
|
272
|
+
expect(synced).toBeTruthy();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("task.sync with ID updates existing node", async () => {
|
|
276
|
+
const created = await createTask(daemon.socketPath, {
|
|
277
|
+
title: "Task to sync-update",
|
|
278
|
+
status: "open",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const ok = await pushSyncEvent(daemon.socketPath, {
|
|
282
|
+
type: "task.sync",
|
|
283
|
+
id: created.id,
|
|
284
|
+
subject: "Updated via sync",
|
|
285
|
+
status: "in_progress",
|
|
286
|
+
source: "map-bridge",
|
|
287
|
+
});
|
|
288
|
+
expect(ok).toBe(true);
|
|
289
|
+
|
|
290
|
+
const stored = daemon.nodes.get(created.id);
|
|
291
|
+
expect(stored.status).toBe("in_progress");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("task.claimed updates assignee", async () => {
|
|
295
|
+
const created = await createTask(daemon.socketPath, {
|
|
296
|
+
title: "Task to claim",
|
|
297
|
+
status: "open",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const ok = await pushSyncEvent(daemon.socketPath, {
|
|
301
|
+
type: "task.claimed",
|
|
302
|
+
id: created.id,
|
|
303
|
+
agent: "gsd-executor",
|
|
304
|
+
source: "map-bridge",
|
|
305
|
+
});
|
|
306
|
+
expect(ok).toBe(true);
|
|
307
|
+
|
|
308
|
+
const stored = daemon.nodes.get(created.id);
|
|
309
|
+
expect(stored.status).toBe("in_progress");
|
|
310
|
+
expect(stored.assignee).toBe("gsd-executor");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("task.unblocked resets status to open", async () => {
|
|
314
|
+
const created = await createTask(daemon.socketPath, {
|
|
315
|
+
title: "Blocked task",
|
|
316
|
+
status: "blocked",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const ok = await pushSyncEvent(daemon.socketPath, {
|
|
320
|
+
type: "task.unblocked",
|
|
321
|
+
id: created.id,
|
|
322
|
+
unblockedBy: "gsd-debugger",
|
|
323
|
+
source: "map-bridge",
|
|
324
|
+
});
|
|
325
|
+
expect(ok).toBe(true);
|
|
326
|
+
|
|
327
|
+
const stored = daemon.nodes.get(created.id);
|
|
328
|
+
expect(stored.status).toBe("open");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("task.linked creates an edge", async () => {
|
|
332
|
+
const from = await createTask(daemon.socketPath, { title: "From", status: "open" });
|
|
333
|
+
const to = await createTask(daemon.socketPath, { title: "To", status: "open" });
|
|
334
|
+
|
|
335
|
+
const ok = await pushSyncEvent(daemon.socketPath, {
|
|
336
|
+
type: "task.linked",
|
|
337
|
+
from: from.id,
|
|
338
|
+
to: to.id,
|
|
339
|
+
linkType: "blocks",
|
|
340
|
+
source: "map-bridge",
|
|
341
|
+
});
|
|
342
|
+
expect(ok).toBe(true);
|
|
343
|
+
|
|
344
|
+
expect(daemon.edges.length).toBeGreaterThan(0);
|
|
345
|
+
const edge = daemon.edges.find((e) => e.fromId === from.id && e.toId === to.id);
|
|
346
|
+
expect(edge).toBeTruthy();
|
|
347
|
+
expect(edge.type).toBe("blocks");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("unknown event type returns false", async () => {
|
|
351
|
+
const ok = await pushSyncEvent(daemon.socketPath, {
|
|
352
|
+
type: "task.unknown_event",
|
|
353
|
+
id: "fake",
|
|
354
|
+
});
|
|
355
|
+
expect(ok).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("pushSyncEvent with dead socket does not throw", async () => {
|
|
359
|
+
// task.sync always returns true (best-effort pattern: fire-and-forget create)
|
|
360
|
+
// The key assertion is that it doesn't throw
|
|
361
|
+
const ok = await pushSyncEvent("/tmp/no-daemon-" + Date.now() + ".sock", {
|
|
362
|
+
type: "task.sync",
|
|
363
|
+
uri: "test://fail",
|
|
364
|
+
status: "open",
|
|
365
|
+
source: "test",
|
|
366
|
+
});
|
|
367
|
+
expect(typeof ok).toBe("boolean");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("task.claimed with dead socket returns false for ID-required events", async () => {
|
|
371
|
+
const ok = await pushSyncEvent("/tmp/no-daemon-" + Date.now() + ".sock", {
|
|
372
|
+
type: "task.claimed",
|
|
373
|
+
id: "fake-id",
|
|
374
|
+
agent: "test",
|
|
375
|
+
source: "test",
|
|
376
|
+
});
|
|
377
|
+
// task.claimed calls rpcRequest which returns null, but pushSyncEvent still returns true
|
|
378
|
+
// The key behavior: never throws
|
|
379
|
+
expect(typeof ok).toBe("boolean");
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
385
|
+
// Group 5: MAP Bridge — task events emitted to MAP server via sidecar
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe(
|
|
389
|
+
"tier7: opentasks MAP bridge events",
|
|
390
|
+
{ timeout: 60_000 },
|
|
391
|
+
() => {
|
|
392
|
+
let mockServer;
|
|
393
|
+
let workspace;
|
|
394
|
+
let sidecar;
|
|
395
|
+
|
|
396
|
+
beforeAll(async () => {
|
|
397
|
+
mockServer = new MockMapServer();
|
|
398
|
+
await mockServer.start();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
afterAll(async () => {
|
|
402
|
+
if (sidecar) sidecar.cleanup();
|
|
403
|
+
if (workspace) workspace.cleanup();
|
|
404
|
+
if (mockServer) await mockServer.stop();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("bridge-task-created event reaches MAP server", async () => {
|
|
408
|
+
workspace = createWorkspace({
|
|
409
|
+
tmpdir: SHORT_TMPDIR,
|
|
410
|
+
prefix: "t7-ot-bridge-",
|
|
411
|
+
config: {
|
|
412
|
+
template: "gsd",
|
|
413
|
+
map: { enabled: true, server: `ws://localhost:${mockServer.port}` },
|
|
414
|
+
opentasks: { enabled: true },
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
sidecar = await startTestSidecar({
|
|
419
|
+
workspaceDir: workspace.dir,
|
|
420
|
+
mockServerPort: mockServer.port,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const resp = await sendCommand(sidecar.socketPath, {
|
|
424
|
+
action: "emit",
|
|
425
|
+
event: {
|
|
426
|
+
type: "bridge-task-created",
|
|
427
|
+
taskId: "task-ot-1",
|
|
428
|
+
title: "Task created from opentasks bridge",
|
|
429
|
+
assignee: "gsd-executor",
|
|
430
|
+
source: "opentasks",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
expect(resp.ok).toBe(true);
|
|
434
|
+
|
|
435
|
+
await waitFor(() => mockServer.sentMessages.length > 0, 5000);
|
|
436
|
+
expect(mockServer.sentMessages.length).toBeGreaterThan(0);
|
|
437
|
+
|
|
438
|
+
const taskEvent = mockServer.sentMessages.find(
|
|
439
|
+
(m) => m.payload?.type === "bridge-task-created"
|
|
440
|
+
);
|
|
441
|
+
expect(taskEvent).toBeTruthy();
|
|
442
|
+
expect(taskEvent.payload.taskId).toBe("task-ot-1");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("bridge-task-status event reaches MAP server", async () => {
|
|
446
|
+
mockServer.clearMessages();
|
|
447
|
+
|
|
448
|
+
const resp = await sendCommand(sidecar.socketPath, {
|
|
449
|
+
action: "emit",
|
|
450
|
+
event: {
|
|
451
|
+
type: "bridge-task-status",
|
|
452
|
+
taskId: "task-ot-1",
|
|
453
|
+
status: "completed",
|
|
454
|
+
assignee: "gsd-executor",
|
|
455
|
+
source: "opentasks",
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
expect(resp.ok).toBe(true);
|
|
459
|
+
|
|
460
|
+
await waitFor(() => mockServer.sentMessages.length > 0, 5000);
|
|
461
|
+
const statusEvent = mockServer.sentMessages.find(
|
|
462
|
+
(m) => m.payload?.type === "bridge-task-status"
|
|
463
|
+
);
|
|
464
|
+
expect(statusEvent).toBeTruthy();
|
|
465
|
+
expect(statusEvent.payload.status).toBe("completed");
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
471
|
+
// Group 6: Context Output — buildCapabilitiesContext includes opentasks
|
|
472
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
describe(
|
|
475
|
+
"tier7: opentasks context output",
|
|
476
|
+
{ timeout: 15_000 },
|
|
477
|
+
() => {
|
|
478
|
+
it("includes opentasks MCP tools when enabled and connected", () => {
|
|
479
|
+
const context = buildCapabilitiesContext({
|
|
480
|
+
opentasksEnabled: true,
|
|
481
|
+
opentasksStatus: "connected",
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(context).toContain("opentasks MCP tools");
|
|
485
|
+
expect(context).toContain("opentasks__create_task");
|
|
486
|
+
expect(context).toContain("opentasks__update_task");
|
|
487
|
+
expect(context).toContain("opentasks__list_tasks");
|
|
488
|
+
expect(context).toContain("opentasks__query");
|
|
489
|
+
expect(context).toContain("opentasks__link");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("shows native task tools when opentasks disabled", () => {
|
|
493
|
+
const context = buildCapabilitiesContext({
|
|
494
|
+
opentasksEnabled: false,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(context).toContain("TaskCreate");
|
|
498
|
+
expect(context).toContain("TaskUpdate");
|
|
499
|
+
expect(context).toContain("TaskList");
|
|
500
|
+
expect(context).not.toContain("opentasks MCP tools");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("shows native task tools when opentasks enabled but not connected", () => {
|
|
504
|
+
const context = buildCapabilitiesContext({
|
|
505
|
+
opentasksEnabled: true,
|
|
506
|
+
opentasksStatus: "starting",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(context).toContain("TaskCreate");
|
|
510
|
+
expect(context).not.toContain("opentasks MCP tools");
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
);
|