macro-agent 0.1.3 → 0.1.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/dist/cognitive/workspace-handler.d.ts +9 -17
- package/dist/cognitive/workspace-handler.d.ts.map +1 -1
- package/dist/cognitive/workspace-handler.js +11 -10
- package/dist/cognitive/workspace-handler.js.map +1 -1
- package/dist/map/coordination-handler.d.ts +23 -7
- package/dist/map/coordination-handler.d.ts.map +1 -1
- package/dist/map/coordination-handler.js +100 -124
- package/dist/map/coordination-handler.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +15 -3
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/trajectory-reporter.d.ts +9 -4
- package/dist/map/trajectory-reporter.d.ts.map +1 -1
- package/dist/map/trajectory-reporter.js +129 -15
- package/dist/map/trajectory-reporter.js.map +1 -1
- package/dist/map/types.d.ts +0 -37
- package/dist/map/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/e2e/cognitive-workspace.e2e.test.ts +1 -1
- package/src/__tests__/e2e/trajectory-content.e2e.test.ts +708 -0
- package/src/cognitive/__tests__/workspace-handler.test.ts +10 -2
- package/src/cognitive/workspace-handler.ts +15 -18
- package/src/map/__tests__/coordination-handler.test.ts +598 -0
- package/src/map/__tests__/trajectory-reporter.test.ts +254 -2
- package/src/map/coordination-handler.ts +120 -137
- package/src/map/sidecar.ts +20 -3
- package/src/map/trajectory-reporter.ts +154 -16
- package/src/map/types.ts +2 -40
|
@@ -52,7 +52,15 @@ function createMockBackend(session?: CognitiveAgentSession): MacroAgentBackend {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
describe("isWorkspaceExecuteMessage", () => {
|
|
55
|
-
it("should return true for workspace.execute
|
|
55
|
+
it("should return true for x-workspace/task.execute", () => {
|
|
56
|
+
expect(
|
|
57
|
+
isWorkspaceExecuteMessage({
|
|
58
|
+
method: "x-workspace/task.execute",
|
|
59
|
+
}),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return true for legacy x-openhive/learning.workspace.execute", () => {
|
|
56
64
|
expect(
|
|
57
65
|
isWorkspaceExecuteMessage({
|
|
58
66
|
method: "x-openhive/learning.workspace.execute",
|
|
@@ -111,7 +119,7 @@ describe("handleWorkspaceExecute", () => {
|
|
|
111
119
|
expect(sentMessages.length).toBe(1);
|
|
112
120
|
const result = sentMessages[0] as any;
|
|
113
121
|
expect(result.jsonrpc).toBe("2.0");
|
|
114
|
-
expect(result.method).toBe("x-
|
|
122
|
+
expect(result.method).toBe("x-workspace/task.result");
|
|
115
123
|
expect(result.params.request_id).toBe("req-001");
|
|
116
124
|
expect(result.params.success).toBe(true);
|
|
117
125
|
expect(result.params.duration_ms).toBeGreaterThanOrEqual(0);
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace Execution Handler
|
|
3
3
|
*
|
|
4
|
-
* Bridge between
|
|
4
|
+
* Bridge between workspace task execution MAP messages and
|
|
5
5
|
* macro-agent's MacroAgentBackend. Receives workspace tasks from
|
|
6
|
-
*
|
|
6
|
+
* a hub, spawns analyst agents, and sends results back.
|
|
7
7
|
*
|
|
8
8
|
* Registered as a MAP notification handler on the swarm's inbound
|
|
9
|
-
* WebSocket connection to the
|
|
9
|
+
* WebSocket connection to the hub.
|
|
10
10
|
*
|
|
11
|
-
* Protocol:
|
|
12
|
-
*
|
|
11
|
+
* Protocol (defined by agent-workspace):
|
|
12
|
+
* Hub → Swarm: x-workspace/task.execute
|
|
13
13
|
* { request_id, prompt, cwd, system_context, timeout }
|
|
14
|
-
* Swarm →
|
|
14
|
+
* Swarm → Hub: x-workspace/task.result
|
|
15
15
|
* { request_id, success, output, structured, duration_ms }
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { WORKSPACE_METHODS, WORKSPACE_METHODS_LEGACY } from "agent-workspace";
|
|
19
|
+
import type { WorkspaceExecuteParams, WorkspaceResultParams } from "agent-workspace";
|
|
18
20
|
import type { MacroAgentBackend } from "./macro-agent-backend.js";
|
|
19
21
|
import type { CognitiveAgentSpawnConfig } from "./types.js";
|
|
20
22
|
|
|
@@ -24,13 +26,8 @@ export interface WorkspaceHandlerDeps {
|
|
|
24
26
|
sendToHub: (message: object) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
prompt: string;
|
|
30
|
-
cwd: string;
|
|
31
|
-
system_context?: string;
|
|
32
|
-
timeout?: number;
|
|
33
|
-
}
|
|
29
|
+
// Re-export the protocol type for consumers
|
|
30
|
+
export type { WorkspaceExecuteParams } from "agent-workspace";
|
|
34
31
|
|
|
35
32
|
/**
|
|
36
33
|
* Handle an incoming workspace.execute request from OpenHive.
|
|
@@ -79,7 +76,7 @@ export async function handleWorkspaceExecute(
|
|
|
79
76
|
await backend.terminate(session.id).catch(() => {});
|
|
80
77
|
sendToHub({
|
|
81
78
|
jsonrpc: "2.0",
|
|
82
|
-
method:
|
|
79
|
+
method: WORKSPACE_METHODS.RESULT,
|
|
83
80
|
params: {
|
|
84
81
|
request_id,
|
|
85
82
|
success: false,
|
|
@@ -128,7 +125,7 @@ export async function handleWorkspaceExecute(
|
|
|
128
125
|
|
|
129
126
|
sendToHub({
|
|
130
127
|
jsonrpc: "2.0",
|
|
131
|
-
method:
|
|
128
|
+
method: WORKSPACE_METHODS.RESULT,
|
|
132
129
|
params: {
|
|
133
130
|
request_id,
|
|
134
131
|
success: finalSession.state === "completed",
|
|
@@ -141,7 +138,7 @@ export async function handleWorkspaceExecute(
|
|
|
141
138
|
} catch (err) {
|
|
142
139
|
sendToHub({
|
|
143
140
|
jsonrpc: "2.0",
|
|
144
|
-
method:
|
|
141
|
+
method: WORKSPACE_METHODS.RESULT,
|
|
145
142
|
params: {
|
|
146
143
|
request_id,
|
|
147
144
|
success: false,
|
|
@@ -158,6 +155,6 @@ export async function handleWorkspaceExecute(
|
|
|
158
155
|
*/
|
|
159
156
|
export function isWorkspaceExecuteMessage(
|
|
160
157
|
msg: { method?: string },
|
|
161
|
-
):
|
|
162
|
-
return msg.method ===
|
|
158
|
+
): boolean {
|
|
159
|
+
return msg.method === WORKSPACE_METHODS.EXECUTE || msg.method === WORKSPACE_METHODS_LEGACY.EXECUTE;
|
|
163
160
|
}
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Coordination Handler — inbound MAP task messages + notifications.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
setupCoordinationHandlers,
|
|
8
|
+
type CoordinationConnection,
|
|
9
|
+
type CoordinationDeps,
|
|
10
|
+
type MAPMessage,
|
|
11
|
+
} from "../coordination-handler.js";
|
|
12
|
+
import type { InboxAdapter, TasksAdapter } from "../../adapters/types.js";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Mock helpers
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
type MessageHandler = (message: MAPMessage) => void | Promise<void>;
|
|
19
|
+
type NotificationHandler = (params: unknown) => void | Promise<void>;
|
|
20
|
+
|
|
21
|
+
function mockConnection() {
|
|
22
|
+
const messageHandlers = new Set<MessageHandler>();
|
|
23
|
+
const notificationHandlers = new Map<string, Set<NotificationHandler>>();
|
|
24
|
+
|
|
25
|
+
const conn: CoordinationConnection = {
|
|
26
|
+
onMessage(handler: MessageHandler) {
|
|
27
|
+
messageHandlers.add(handler);
|
|
28
|
+
},
|
|
29
|
+
offMessage(handler: MessageHandler) {
|
|
30
|
+
messageHandlers.delete(handler);
|
|
31
|
+
},
|
|
32
|
+
onNotification(method: string, handler: NotificationHandler) {
|
|
33
|
+
if (!notificationHandlers.has(method)) {
|
|
34
|
+
notificationHandlers.set(method, new Set());
|
|
35
|
+
}
|
|
36
|
+
notificationHandlers.get(method)!.add(handler);
|
|
37
|
+
},
|
|
38
|
+
offNotification(method: string, handler: NotificationHandler) {
|
|
39
|
+
notificationHandlers.get(method)?.delete(handler);
|
|
40
|
+
},
|
|
41
|
+
sendNotification: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
conn,
|
|
46
|
+
messageHandlers,
|
|
47
|
+
notificationHandlers,
|
|
48
|
+
/** Simulate an incoming MAP scope message */
|
|
49
|
+
async emitMessage(payload: Record<string, unknown>) {
|
|
50
|
+
const msg: MAPMessage = {
|
|
51
|
+
id: `msg-${Date.now()}`,
|
|
52
|
+
from: "hub",
|
|
53
|
+
to: { scope: "swarm:test" },
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
payload,
|
|
56
|
+
};
|
|
57
|
+
for (const handler of messageHandlers) {
|
|
58
|
+
await handler(msg);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
/** Simulate an incoming JSON-RPC notification */
|
|
62
|
+
async emitNotification(method: string, params: unknown) {
|
|
63
|
+
const handlers = notificationHandlers.get(method);
|
|
64
|
+
if (handlers) {
|
|
65
|
+
for (const handler of handlers) {
|
|
66
|
+
await handler(params);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mockInboxAdapter() {
|
|
74
|
+
return {
|
|
75
|
+
send: vi.fn().mockResolvedValue("msg-1"),
|
|
76
|
+
} as unknown as InboxAdapter;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mockTasksAdapter() {
|
|
80
|
+
return {
|
|
81
|
+
createTask: vi.fn().mockResolvedValue("task-100"),
|
|
82
|
+
assignTask: vi.fn().mockResolvedValue(undefined),
|
|
83
|
+
transitionTask: vi.fn().mockResolvedValue(undefined),
|
|
84
|
+
connected: true,
|
|
85
|
+
} as unknown as TasksAdapter;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createDeps(overrides: Partial<CoordinationDeps> = {}): CoordinationDeps & {
|
|
89
|
+
mock: ReturnType<typeof mockConnection>;
|
|
90
|
+
} {
|
|
91
|
+
const mock = mockConnection();
|
|
92
|
+
return {
|
|
93
|
+
connection: mock.conn,
|
|
94
|
+
inboxAdapter: mockInboxAdapter(),
|
|
95
|
+
tasksAdapter: mockTasksAdapter(),
|
|
96
|
+
...overrides,
|
|
97
|
+
mock,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Tests — Task messages (MAP scope messages)
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
describe("CoordinationHandler — task messages", () => {
|
|
106
|
+
let deps: ReturnType<typeof createDeps>;
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
deps = createDeps();
|
|
110
|
+
setupCoordinationHandlers(deps);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("task.created", () => {
|
|
114
|
+
it("creates task in opentasks", async () => {
|
|
115
|
+
await deps.mock.emitMessage({
|
|
116
|
+
type: "task.created",
|
|
117
|
+
task: { id: "t-1", title: "Fix bug", status: "open", assignee: "agent-1" },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(deps.tasksAdapter.createTask).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
title: "Fix bug",
|
|
123
|
+
assignee: "agent-1",
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("notifies assignee via inbox", async () => {
|
|
129
|
+
await deps.mock.emitMessage({
|
|
130
|
+
type: "task.created",
|
|
131
|
+
task: { id: "t-1", title: "Fix bug", status: "open", assignee: "agent-1" },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(deps.inboxAdapter.send).toHaveBeenCalledWith(
|
|
135
|
+
"system",
|
|
136
|
+
"agent-1",
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
type: "event",
|
|
139
|
+
event: "TASK_ASSIGNED",
|
|
140
|
+
data: expect.objectContaining({ title: "Fix bug" }),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("skips inbox notification when no assignee", async () => {
|
|
146
|
+
await deps.mock.emitMessage({
|
|
147
|
+
type: "task.created",
|
|
148
|
+
task: { id: "t-1", title: "Unassigned task", status: "open" },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
|
|
152
|
+
expect(deps.inboxAdapter.send).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("passes task description to opentasks", async () => {
|
|
156
|
+
await deps.mock.emitMessage({
|
|
157
|
+
type: "task.created",
|
|
158
|
+
task: { id: "t-1", title: "Fix bug", status: "open", description: "Segfault on startup" },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(deps.tasksAdapter.createTask).toHaveBeenCalledWith(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
title: "Fix bug",
|
|
164
|
+
content: "Segfault on startup",
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("ignores messages without a title", async () => {
|
|
170
|
+
await deps.mock.emitMessage({
|
|
171
|
+
type: "task.created",
|
|
172
|
+
task: { id: "t-1", status: "open" },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("ignores messages without a task object", async () => {
|
|
179
|
+
await deps.mock.emitMessage({ type: "task.created" });
|
|
180
|
+
|
|
181
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("task.assigned", () => {
|
|
186
|
+
it("assigns task in opentasks and notifies agent", async () => {
|
|
187
|
+
await deps.mock.emitMessage({
|
|
188
|
+
type: "task.assigned",
|
|
189
|
+
taskId: "t-1",
|
|
190
|
+
assignee: "agent-2",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(deps.tasksAdapter.assignTask).toHaveBeenCalledWith("t-1", "agent-2");
|
|
194
|
+
expect(deps.inboxAdapter.send).toHaveBeenCalledWith(
|
|
195
|
+
"system",
|
|
196
|
+
"agent-2",
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
event: "TASK_ASSIGNED",
|
|
199
|
+
data: expect.objectContaining({ taskId: "t-1" }),
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("ignores messages without taskId", async () => {
|
|
205
|
+
await deps.mock.emitMessage({
|
|
206
|
+
type: "task.assigned",
|
|
207
|
+
assignee: "agent-2",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(deps.tasksAdapter.assignTask).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("ignores messages without assignee", async () => {
|
|
214
|
+
await deps.mock.emitMessage({
|
|
215
|
+
type: "task.assigned",
|
|
216
|
+
taskId: "t-1",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(deps.tasksAdapter.assignTask).not.toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("task.status", () => {
|
|
224
|
+
it("transitions task to in_progress", async () => {
|
|
225
|
+
await deps.mock.emitMessage({
|
|
226
|
+
type: "task.status",
|
|
227
|
+
taskId: "t-1",
|
|
228
|
+
previous: "open",
|
|
229
|
+
current: "in_progress",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "start");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("transitions task to completed", async () => {
|
|
236
|
+
await deps.mock.emitMessage({
|
|
237
|
+
type: "task.status",
|
|
238
|
+
taskId: "t-1",
|
|
239
|
+
previous: "in_progress",
|
|
240
|
+
current: "completed",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "complete");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("transitions task to blocked", async () => {
|
|
247
|
+
await deps.mock.emitMessage({
|
|
248
|
+
type: "task.status",
|
|
249
|
+
taskId: "t-1",
|
|
250
|
+
previous: "in_progress",
|
|
251
|
+
current: "blocked",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "block");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("transitions task to failed", async () => {
|
|
258
|
+
await deps.mock.emitMessage({
|
|
259
|
+
type: "task.status",
|
|
260
|
+
taskId: "t-1",
|
|
261
|
+
previous: "in_progress",
|
|
262
|
+
current: "failed",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "fail");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("transitions closed to complete", async () => {
|
|
269
|
+
await deps.mock.emitMessage({
|
|
270
|
+
type: "task.status",
|
|
271
|
+
taskId: "t-1",
|
|
272
|
+
previous: "in_progress",
|
|
273
|
+
current: "closed",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "complete");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("reopens task", async () => {
|
|
280
|
+
await deps.mock.emitMessage({
|
|
281
|
+
type: "task.status",
|
|
282
|
+
taskId: "t-1",
|
|
283
|
+
previous: "blocked",
|
|
284
|
+
current: "open",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(deps.tasksAdapter.transitionTask).toHaveBeenCalledWith("t-1", "reopen");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("ignores unknown status values", async () => {
|
|
291
|
+
await deps.mock.emitMessage({
|
|
292
|
+
type: "task.status",
|
|
293
|
+
taskId: "t-1",
|
|
294
|
+
previous: "open",
|
|
295
|
+
current: "unknown_status",
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("ignores messages without taskId", async () => {
|
|
302
|
+
await deps.mock.emitMessage({
|
|
303
|
+
type: "task.status",
|
|
304
|
+
current: "completed",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("ignores messages without current status", async () => {
|
|
311
|
+
await deps.mock.emitMessage({
|
|
312
|
+
type: "task.status",
|
|
313
|
+
taskId: "t-1",
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("echo prevention", () => {
|
|
321
|
+
it("skips messages with _origin macro-agent", async () => {
|
|
322
|
+
await deps.mock.emitMessage({
|
|
323
|
+
type: "task.created",
|
|
324
|
+
task: { id: "t-1", title: "My own task", status: "open" },
|
|
325
|
+
_origin: "macro-agent",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("processes messages from other origins", async () => {
|
|
332
|
+
await deps.mock.emitMessage({
|
|
333
|
+
type: "task.created",
|
|
334
|
+
task: { id: "t-1", title: "External task", status: "open" },
|
|
335
|
+
_origin: "cc-swarm",
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("processes messages with no origin", async () => {
|
|
342
|
+
await deps.mock.emitMessage({
|
|
343
|
+
type: "task.created",
|
|
344
|
+
task: { id: "t-1", title: "No origin task", status: "open" },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("error handling", () => {
|
|
352
|
+
it("warns on tasksAdapter failure without throwing", async () => {
|
|
353
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
354
|
+
(deps.tasksAdapter.createTask as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
355
|
+
new Error("opentasks down"),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
await deps.mock.emitMessage({
|
|
359
|
+
type: "task.created",
|
|
360
|
+
task: { id: "t-1", title: "Will fail", status: "open" },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
364
|
+
expect.stringContaining("opentasks down"),
|
|
365
|
+
);
|
|
366
|
+
warnSpy.mockRestore();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("continues processing after inbox send failure", async () => {
|
|
370
|
+
(deps.inboxAdapter.send as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
371
|
+
new Error("inbox error"),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Should not throw — inbox errors are caught
|
|
375
|
+
await deps.mock.emitMessage({
|
|
376
|
+
type: "task.created",
|
|
377
|
+
task: { id: "t-1", title: "Task", status: "open", assignee: "agent-1" },
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(deps.tasksAdapter.createTask).toHaveBeenCalled();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("ignored message types", () => {
|
|
385
|
+
it("ignores task.completed (informational only)", async () => {
|
|
386
|
+
await deps.mock.emitMessage({
|
|
387
|
+
type: "task.completed",
|
|
388
|
+
taskId: "t-1",
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
392
|
+
expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("ignores messages without payload", async () => {
|
|
396
|
+
const msg: MAPMessage = {
|
|
397
|
+
id: "msg-1",
|
|
398
|
+
from: "hub",
|
|
399
|
+
to: { scope: "swarm:test" },
|
|
400
|
+
timestamp: new Date().toISOString(),
|
|
401
|
+
};
|
|
402
|
+
for (const handler of deps.mock.messageHandlers) {
|
|
403
|
+
await handler(msg);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("ignores messages without type field", async () => {
|
|
410
|
+
await deps.mock.emitMessage({ taskId: "t-1", status: "open" });
|
|
411
|
+
|
|
412
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// =============================================================================
|
|
418
|
+
// Tests — Context and messaging NOT handled here (agent-inbox flow)
|
|
419
|
+
// =============================================================================
|
|
420
|
+
|
|
421
|
+
describe("CoordinationHandler — context/messaging excluded", () => {
|
|
422
|
+
let deps: ReturnType<typeof createDeps>;
|
|
423
|
+
|
|
424
|
+
beforeEach(() => {
|
|
425
|
+
deps = createDeps();
|
|
426
|
+
setupCoordinationHandlers(deps);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("does not handle context.shared messages (handled by agent-inbox)", async () => {
|
|
430
|
+
await deps.mock.emitMessage({
|
|
431
|
+
type: "context.shared",
|
|
432
|
+
context_type: "code_review",
|
|
433
|
+
data: { file: "main.ts" },
|
|
434
|
+
source_swarm_id: "swarm-A",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// No task adapter calls
|
|
438
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
439
|
+
expect(deps.tasksAdapter.transitionTask).not.toHaveBeenCalled();
|
|
440
|
+
// No inbox calls from coordination handler
|
|
441
|
+
expect(deps.inboxAdapter.send).not.toHaveBeenCalled();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("does not handle message type messages (handled by agent-inbox)", async () => {
|
|
445
|
+
await deps.mock.emitMessage({
|
|
446
|
+
type: "message",
|
|
447
|
+
from_swarm_id: "swarm-A",
|
|
448
|
+
to_swarm_id: "agent-1",
|
|
449
|
+
content_type: "text",
|
|
450
|
+
content: "Hello",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(deps.tasksAdapter.createTask).not.toHaveBeenCalled();
|
|
454
|
+
expect(deps.inboxAdapter.send).not.toHaveBeenCalled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("does not register x-openhive/context.share notification handler", () => {
|
|
458
|
+
expect(deps.mock.notificationHandlers.has("x-openhive/context.share")).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("does not register x-openhive/message.send notification handler", () => {
|
|
462
|
+
expect(deps.mock.notificationHandlers.has("x-openhive/message.send")).toBe(false);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("does not register x-openhive/task.assign notification handler", () => {
|
|
466
|
+
expect(deps.mock.notificationHandlers.has("x-openhive/task.assign")).toBe(false);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("does not register x-openhive/task.status notification handler", () => {
|
|
470
|
+
expect(deps.mock.notificationHandlers.has("x-openhive/task.status")).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// =============================================================================
|
|
475
|
+
// Tests — Workspace notifications
|
|
476
|
+
// =============================================================================
|
|
477
|
+
|
|
478
|
+
describe("CoordinationHandler — workspace notifications", () => {
|
|
479
|
+
let deps: ReturnType<typeof createDeps>;
|
|
480
|
+
|
|
481
|
+
describe("workspace.execute", () => {
|
|
482
|
+
it("delegates to workspace handler via x-workspace/task.execute", async () => {
|
|
483
|
+
const handleWorkspaceExecute = vi.fn().mockResolvedValue(undefined);
|
|
484
|
+
deps = createDeps({
|
|
485
|
+
workspaceHandler: {
|
|
486
|
+
handleWorkspaceExecute,
|
|
487
|
+
isWorkspaceExecuteMessage: (msg) => msg.method === "x-workspace/task.execute",
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
setupCoordinationHandlers(deps);
|
|
491
|
+
|
|
492
|
+
const params = { request_id: "req-1", prompt: "Do thing", cwd: "/tmp" };
|
|
493
|
+
await deps.mock.emitNotification("x-workspace/task.execute", params);
|
|
494
|
+
|
|
495
|
+
expect(handleWorkspaceExecute).toHaveBeenCalledWith(params);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("delegates legacy x-openhive/learning.workspace.execute", async () => {
|
|
499
|
+
const handleWorkspaceExecute = vi.fn().mockResolvedValue(undefined);
|
|
500
|
+
deps = createDeps({
|
|
501
|
+
workspaceHandler: {
|
|
502
|
+
handleWorkspaceExecute,
|
|
503
|
+
isWorkspaceExecuteMessage: () => true,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
setupCoordinationHandlers(deps);
|
|
507
|
+
|
|
508
|
+
const params = { request_id: "req-2", prompt: "Legacy task", cwd: "/tmp" };
|
|
509
|
+
await deps.mock.emitNotification("x-openhive/learning.workspace.execute", params);
|
|
510
|
+
|
|
511
|
+
expect(handleWorkspaceExecute).toHaveBeenCalledWith(params);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("warns on workspace handler failure without throwing", async () => {
|
|
515
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
516
|
+
const handleWorkspaceExecute = vi.fn().mockRejectedValue(new Error("spawn failed"));
|
|
517
|
+
deps = createDeps({
|
|
518
|
+
workspaceHandler: {
|
|
519
|
+
handleWorkspaceExecute,
|
|
520
|
+
isWorkspaceExecuteMessage: () => true,
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
setupCoordinationHandlers(deps);
|
|
524
|
+
|
|
525
|
+
await deps.mock.emitNotification("x-workspace/task.execute", {
|
|
526
|
+
request_id: "req-3",
|
|
527
|
+
prompt: "fail",
|
|
528
|
+
cwd: "/tmp",
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("spawn failed"));
|
|
532
|
+
warnSpy.mockRestore();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("does not register workspace handlers when no workspace handler provided", () => {
|
|
536
|
+
deps = createDeps();
|
|
537
|
+
setupCoordinationHandlers(deps);
|
|
538
|
+
|
|
539
|
+
expect(deps.mock.notificationHandlers.has("x-workspace/task.execute")).toBe(false);
|
|
540
|
+
expect(deps.mock.notificationHandlers.has("x-openhive/learning.workspace.execute")).toBe(false);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// =============================================================================
|
|
546
|
+
// Tests — Handler registration and cleanup
|
|
547
|
+
// =============================================================================
|
|
548
|
+
|
|
549
|
+
describe("CoordinationHandler — cleanup", () => {
|
|
550
|
+
it("removes all handlers on cleanup", () => {
|
|
551
|
+
const deps = createDeps({
|
|
552
|
+
workspaceHandler: {
|
|
553
|
+
handleWorkspaceExecute: vi.fn(),
|
|
554
|
+
isWorkspaceExecuteMessage: () => true,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
const cleanup = setupCoordinationHandlers(deps);
|
|
558
|
+
|
|
559
|
+
// Verify handlers were registered
|
|
560
|
+
expect(deps.mock.messageHandlers.size).toBe(1);
|
|
561
|
+
expect(deps.mock.notificationHandlers.size).toBeGreaterThan(0);
|
|
562
|
+
|
|
563
|
+
cleanup();
|
|
564
|
+
|
|
565
|
+
// All handlers removed
|
|
566
|
+
expect(deps.mock.messageHandlers.size).toBe(0);
|
|
567
|
+
for (const [, handlers] of deps.mock.notificationHandlers) {
|
|
568
|
+
expect(handlers.size).toBe(0);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("only registers message handler and workspace notifications", () => {
|
|
573
|
+
const deps = createDeps({
|
|
574
|
+
workspaceHandler: {
|
|
575
|
+
handleWorkspaceExecute: vi.fn(),
|
|
576
|
+
isWorkspaceExecuteMessage: () => true,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
setupCoordinationHandlers(deps);
|
|
580
|
+
|
|
581
|
+
// One message handler for task events
|
|
582
|
+
expect(deps.mock.messageHandlers.size).toBe(1);
|
|
583
|
+
|
|
584
|
+
// Two notification handlers: x-workspace/task.execute + legacy
|
|
585
|
+
const registeredMethods = [...deps.mock.notificationHandlers.keys()];
|
|
586
|
+
expect(registeredMethods).toContain("x-workspace/task.execute");
|
|
587
|
+
expect(registeredMethods).toContain("x-openhive/learning.workspace.execute");
|
|
588
|
+
expect(registeredMethods).toHaveLength(2);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("registers only message handler when no workspace handler", () => {
|
|
592
|
+
const deps = createDeps();
|
|
593
|
+
setupCoordinationHandlers(deps);
|
|
594
|
+
|
|
595
|
+
expect(deps.mock.messageHandlers.size).toBe(1);
|
|
596
|
+
expect(deps.mock.notificationHandlers.size).toBe(0);
|
|
597
|
+
});
|
|
598
|
+
});
|