macro-agent 0.1.0 → 0.1.1
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/settings.local.json +3 -1
- package/.sudocode/specs.jsonl +4 -0
- package/CLAUDE.md +16 -14
- package/README.md +11 -29
- package/dist/acp/macro-agent.d.ts +15 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +131 -35
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +32 -1
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +65 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +464 -183
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +37 -6
- package/dist/api/server.js.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token.d.ts +41 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +73 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/cli/acp.d.ts +2 -23
- package/dist/cli/acp.d.ts.map +1 -1
- package/dist/cli/acp.js +127 -61
- package/dist/cli/acp.js.map +1 -1
- package/dist/cli/index.js +147 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +6 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +268 -181
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/parse-args.d.ts +20 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli/parse-args.js.map +1 -0
- package/dist/cli/stable-instance-id.d.ts +8 -0
- package/dist/cli/stable-instance-id.d.ts.map +1 -0
- package/dist/cli/stable-instance-id.js +14 -0
- package/dist/cli/stable-instance-id.js.map +1 -0
- package/dist/config/project-config.d.ts +74 -7
- package/dist/config/project-config.d.ts.map +1 -1
- package/dist/config/project-config.js +123 -20
- package/dist/config/project-config.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +17 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +384 -23
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/connection-manager.d.ts.map +1 -1
- package/dist/map/adapter/connection-manager.js +3 -0
- package/dist/map/adapter/connection-manager.js.map +1 -1
- package/dist/map/adapter/event-log.d.ts +87 -0
- package/dist/map/adapter/event-log.d.ts.map +1 -0
- package/dist/map/adapter/event-log.js +122 -0
- package/dist/map/adapter/event-log.js.map +1 -0
- package/dist/map/adapter/event-translator.js +6 -6
- package/dist/map/adapter/event-translator.js.map +1 -1
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
- package/dist/map/adapter/extensions/index.d.ts +10 -1
- package/dist/map/adapter/extensions/index.d.ts.map +1 -1
- package/dist/map/adapter/extensions/index.js +34 -0
- package/dist/map/adapter/extensions/index.js.map +1 -1
- package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
- package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
- package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
- package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
- package/dist/map/adapter/extensions/rename.d.ts +29 -0
- package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
- package/dist/map/adapter/extensions/rename.js +49 -0
- package/dist/map/adapter/extensions/rename.js.map +1 -0
- package/dist/map/adapter/extensions/task.d.ts.map +1 -1
- package/dist/map/adapter/extensions/task.js +10 -0
- package/dist/map/adapter/extensions/task.js.map +1 -1
- package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
- package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
- package/dist/map/adapter/extensions/update-metadata.js +67 -0
- package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
- package/dist/map/adapter/index.d.ts +2 -1
- package/dist/map/adapter/index.d.ts.map +1 -1
- package/dist/map/adapter/index.js +8 -2
- package/dist/map/adapter/index.js.map +1 -1
- package/dist/map/adapter/interface.d.ts +2 -0
- package/dist/map/adapter/interface.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.d.ts +3 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +258 -35
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
- package/dist/map/adapter/subscription-manager.js +5 -1
- package/dist/map/adapter/subscription-manager.js.map +1 -1
- package/dist/map/adapter/types.d.ts +2 -0
- package/dist/map/adapter/types.d.ts.map +1 -1
- package/dist/mcp/map-client.d.ts +39 -0
- package/dist/mcp/map-client.d.ts.map +1 -0
- package/dist/mcp/map-client.js +129 -0
- package/dist/mcp/map-client.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +14 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +113 -85
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/types.d.ts +9 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js.map +1 -1
- package/dist/metrics/metrics.js +1 -1
- package/dist/metrics/metrics.js.map +1 -1
- package/dist/roles/capabilities.d.ts +3 -1
- package/dist/roles/capabilities.d.ts.map +1 -1
- package/dist/roles/capabilities.js +17 -7
- package/dist/roles/capabilities.js.map +1 -1
- package/dist/roles/config-loader.d.ts +6 -6
- package/dist/roles/config-loader.d.ts.map +1 -1
- package/dist/roles/config-loader.js +6 -6
- package/dist/roles/config-loader.js.map +1 -1
- package/dist/roles/registry.d.ts +2 -2
- package/dist/roles/registry.js +2 -2
- package/dist/server/combined-server.d.ts +20 -0
- package/dist/server/combined-server.d.ts.map +1 -1
- package/dist/server/combined-server.js +107 -8
- package/dist/server/combined-server.js.map +1 -1
- package/dist/store/event-store.d.ts +2 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +69 -20
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/types/agents.d.ts +18 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/dist/store/types/events.d.ts +1 -1
- package/dist/store/types/events.d.ts.map +1 -1
- package/dist/task/backend/index.d.ts +47 -29
- package/dist/task/backend/index.d.ts.map +1 -1
- package/dist/task/backend/index.js +109 -71
- package/dist/task/backend/index.js.map +1 -1
- package/dist/task/backend/memory.d.ts +1 -0
- package/dist/task/backend/memory.d.ts.map +1 -1
- package/dist/task/backend/memory.js +3 -0
- package/dist/task/backend/memory.js.map +1 -1
- package/dist/task/backend/opentasks/backend.d.ts +140 -0
- package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
- package/dist/task/backend/opentasks/backend.js +1023 -0
- package/dist/task/backend/opentasks/backend.js.map +1 -0
- package/dist/task/backend/opentasks/client.d.ts +337 -0
- package/dist/task/backend/opentasks/client.d.ts.map +1 -0
- package/dist/task/backend/opentasks/client.js +225 -0
- package/dist/task/backend/opentasks/client.js.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.js +195 -0
- package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
- package/dist/task/backend/opentasks/index.d.ts +21 -0
- package/dist/task/backend/opentasks/index.d.ts.map +1 -0
- package/dist/task/backend/opentasks/index.js +21 -0
- package/dist/task/backend/opentasks/index.js.map +1 -0
- package/dist/task/backend/opentasks/mapping.d.ts +48 -0
- package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
- package/dist/task/backend/opentasks/mapping.js +77 -0
- package/dist/task/backend/opentasks/mapping.js.map +1 -0
- package/dist/task/backend/types.d.ts +33 -53
- package/dist/task/backend/types.d.ts.map +1 -1
- package/dist/task/backend/types.js +7 -11
- package/dist/task/backend/types.js.map +1 -1
- package/dist/task/backend/unified-tool-provider.d.ts +57 -0
- package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
- package/dist/task/backend/unified-tool-provider.js +623 -0
- package/dist/task/backend/unified-tool-provider.js.map +1 -0
- package/dist/teams/team-loader.d.ts +2 -2
- package/dist/teams/team-loader.js +3 -3
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-runtime.d.ts.map +1 -1
- package/dist/teams/team-runtime.js +2 -0
- package/dist/teams/team-runtime.js.map +1 -1
- package/docs/architecture.md +7 -6
- package/docs/configuration.md +26 -62
- package/docs/implementation-details.md +5 -5
- package/docs/implementation-summary.md +17 -17
- package/docs/plan-self-driving-support.md +4 -4
- package/docs/spec-self-driving-support.md +10 -10
- package/docs/team-templates.md +2 -2
- package/docs/teams.md +3 -3
- package/docs/troubleshooting.md +10 -11
- package/package.json +6 -4
- package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
- package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
- package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
- package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
- package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
- package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
- package/src/acp/__tests__/integration.test.ts +56 -31
- package/src/acp/__tests__/macro-agent.test.ts +16 -7
- package/src/acp/macro-agent.ts +170 -36
- package/src/acp/types.ts +46 -1
- package/src/agent/__tests__/agent-manager.test.ts +228 -2
- package/src/agent/agent-manager.ts +714 -261
- package/src/agent/types.ts +3 -1
- package/src/api/server.ts +41 -7
- package/src/auth/__tests__/token.test.ts +100 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/token.ts +82 -0
- package/src/cli/__tests__/acp.test.ts +1 -1
- package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
- package/src/cli/acp.ts +130 -72
- package/src/cli/index.ts +120 -14
- package/src/cli/mcp.ts +311 -207
- package/src/cli/parse-args.ts +54 -0
- package/src/cli/stable-instance-id.ts +14 -0
- package/src/config/project-config.ts +190 -27
- package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
- package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +22 -4
- package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +263 -0
- package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
- package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
- package/src/map/adapter/__tests__/event-log.test.ts +527 -0
- package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
- package/src/map/adapter/__tests__/extensions.test.ts +408 -0
- package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
- package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
- package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
- package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
- package/src/map/adapter/acp-over-map.ts +678 -66
- package/src/map/adapter/connection-manager.ts +3 -0
- package/src/map/adapter/event-log.ts +208 -0
- package/src/map/adapter/event-translator.ts +6 -6
- package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
- package/src/map/adapter/extensions/index.ts +60 -0
- package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
- package/src/map/adapter/extensions/task.ts +11 -0
- package/src/map/adapter/extensions/update-metadata.ts +126 -0
- package/src/map/adapter/index.ts +28 -0
- package/src/map/adapter/interface.ts +2 -0
- package/src/map/adapter/map-adapter.ts +312 -47
- package/src/map/adapter/subscription-manager.ts +5 -1
- package/src/map/adapter/types.ts +2 -0
- package/src/mcp/__tests__/map-client.test.ts +386 -0
- package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
- package/src/mcp/__tests__/mcp-server.test.ts +100 -1
- package/src/mcp/map-client.ts +177 -0
- package/src/mcp/mcp-server.ts +191 -100
- package/src/mcp/types.ts +6 -1
- package/src/metrics/metrics.ts +1 -1
- package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
- package/src/roles/__tests__/config-loader.test.ts +7 -7
- package/src/roles/capabilities.ts +17 -7
- package/src/roles/config-loader.ts +6 -6
- package/src/roles/registry.ts +2 -2
- package/src/server/__tests__/combined-server.test.ts +94 -21
- package/src/server/combined-server.ts +189 -33
- package/src/steering/__tests__/steering-integration.test.ts +1 -1
- package/src/store/__tests__/event-store.test.ts +196 -1
- package/src/store/__tests__/instance.test.ts +3 -3
- package/src/store/event-store.ts +80 -21
- package/src/store/types/agents.ts +15 -0
- package/src/store/types/events.ts +1 -1
- package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
- package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
- package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
- package/src/task/backend/index.ts +156 -106
- package/src/task/backend/memory.ts +4 -0
- package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
- package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
- package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
- package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
- package/src/task/backend/opentasks/backend.ts +1323 -0
- package/src/task/backend/opentasks/client.ts +652 -0
- package/src/task/backend/opentasks/daemon-manager.ts +253 -0
- package/src/task/backend/opentasks/index.ts +69 -0
- package/src/task/backend/opentasks/mapping.ts +94 -0
- package/src/task/backend/types.ts +42 -66
- package/src/task/backend/unified-tool-provider.ts +779 -0
- package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
- package/src/teams/team-loader.ts +3 -3
- package/src/teams/team-runtime.ts +2 -0
- package/test_fixtures/README.md +2 -3
- package/test_fixtures/fixtures/index.ts +0 -3
- package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
- package/test_fixtures/fixtures/repos/index.ts +1 -3
- package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
- package/test_fixtures/fixtures/repos/types.ts +0 -11
- package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
- package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
- package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
- package/vitest.config.ts +1 -1
- package/vitest.e2e.config.ts +1 -1
- package/vitest.setup.ts +1 -30
- package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
- package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
- package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
- package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
- package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
- package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
- package/.macro-agent/teams/self-driving/team.yaml +0 -103
- package/.macro-agent/teams/structured/prompts/developer.md +0 -26
- package/.macro-agent/teams/structured/prompts/lead.md +0 -25
- package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
- package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
- package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
- package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
- package/.macro-agent/teams/structured/team.yaml +0 -89
- package/docs/sudocode-integration.md +0 -383
- package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
- package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
- package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
- package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
- package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
- package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
- package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
- package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
- package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
- package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
- package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
- package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
- package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
- package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
- package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
- package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
- package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
- package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
- package/src/task/backend/sudocode/backend.ts +0 -1237
- package/src/task/backend/sudocode/client.ts +0 -515
- package/src/task/backend/sudocode/index.ts +0 -120
- package/src/task/backend/sudocode/mapping.ts +0 -93
- package/src/task/backend/sudocode/server-client.ts +0 -522
- package/src/task/backend/sudocode/standalone-client.ts +0 -623
- package/src/task/backend/sudocode/sync-policy.ts +0 -387
- package/src/task/backend/sudocode/tools.ts +0 -896
- package/src/task/backend/tool-provider.ts +0 -506
- package/test_fixtures/fixtures/sudocode/index.ts +0 -29
- package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
- package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for MCP Bridge Extensions (_macro/mcp/*)
|
|
3
|
+
*
|
|
4
|
+
* Tests the 17 server-side bridge handlers with mock adapter + mock services.
|
|
5
|
+
* Follows the pattern from extensions.test.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
registerMCPBridgeExtensions,
|
|
11
|
+
unregisterMCPBridgeExtensions,
|
|
12
|
+
MCP_BRIDGE_METHODS,
|
|
13
|
+
type MCPBridgeServices,
|
|
14
|
+
} from "../extensions/mcp-bridge.js";
|
|
15
|
+
import type { MAPAdapter, ExtensionHandler, ExtensionContext } from "../interface.js";
|
|
16
|
+
import type { ParticipantCapabilities } from "../types.js";
|
|
17
|
+
import type { EventStore } from "../../../store/event-store.js";
|
|
18
|
+
import type { AgentManager } from "../../../agent/agent-manager.js";
|
|
19
|
+
import type { TaskManager } from "../../../task/task-manager.js";
|
|
20
|
+
import type { MessageRouter } from "../../../router/message-router.js";
|
|
21
|
+
import type { PeerManager } from "../../../peer/peer-manager.js";
|
|
22
|
+
import type { ActivityWatcher } from "../../../activity/watcher.js";
|
|
23
|
+
import type { TaskBackend } from "../../../task/backend/types.js";
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Mock Setup
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
function createMockAdapter(): MAPAdapter & {
|
|
30
|
+
handlers: Map<string, ExtensionHandler>;
|
|
31
|
+
} {
|
|
32
|
+
const handlers = new Map<string, ExtensionHandler>();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
handlers,
|
|
36
|
+
registerExtension: vi.fn((method: string, handler: ExtensionHandler) => {
|
|
37
|
+
handlers.set(method, handler);
|
|
38
|
+
}),
|
|
39
|
+
unregisterExtension: vi.fn((method: string) => {
|
|
40
|
+
handlers.delete(method);
|
|
41
|
+
}),
|
|
42
|
+
hasExtension: vi.fn((method: string) => handlers.has(method)),
|
|
43
|
+
getExtensions: vi.fn(() => Array.from(handlers.keys())),
|
|
44
|
+
start: vi.fn(),
|
|
45
|
+
stop: vi.fn(),
|
|
46
|
+
isRunning: vi.fn().mockReturnValue(true),
|
|
47
|
+
acceptConnection: vi.fn(),
|
|
48
|
+
disconnectParticipant: vi.fn(),
|
|
49
|
+
getParticipant: vi.fn(),
|
|
50
|
+
getParticipants: vi.fn().mockReturnValue([]),
|
|
51
|
+
createSubscription: vi.fn(),
|
|
52
|
+
removeSubscription: vi.fn(),
|
|
53
|
+
pauseSubscription: vi.fn(),
|
|
54
|
+
resumeSubscription: vi.fn(),
|
|
55
|
+
getSubscriptions: vi.fn().mockReturnValue([]),
|
|
56
|
+
emitEvent: vi.fn(),
|
|
57
|
+
listAgents: vi.fn().mockReturnValue([]),
|
|
58
|
+
getAgent: vi.fn(),
|
|
59
|
+
listScopes: vi.fn().mockReturnValue([]),
|
|
60
|
+
getScope: vi.fn(),
|
|
61
|
+
sendMessage: vi.fn(),
|
|
62
|
+
onEvent: vi.fn().mockReturnValue(() => {}),
|
|
63
|
+
config: { name: "test", version: "1.0.0" },
|
|
64
|
+
} as unknown as MAPAdapter & { handlers: Map<string, ExtensionHandler> };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createMockContext(): ExtensionContext {
|
|
68
|
+
return {
|
|
69
|
+
participantId: "p-test" as any,
|
|
70
|
+
capabilities: {
|
|
71
|
+
canQuery: true,
|
|
72
|
+
canSubscribe: true,
|
|
73
|
+
canMessage: true,
|
|
74
|
+
canManageTasks: true,
|
|
75
|
+
} as ParticipantCapabilities,
|
|
76
|
+
sessionId: "s-test",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createMockEventStore(): EventStore {
|
|
81
|
+
return {
|
|
82
|
+
emit: vi.fn((input) => ({
|
|
83
|
+
id: `evt_${Date.now()}`,
|
|
84
|
+
version: 1,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
...input,
|
|
87
|
+
})),
|
|
88
|
+
query: vi.fn(() => []),
|
|
89
|
+
getAgent: vi.fn(() => null),
|
|
90
|
+
listAgents: vi.fn(() => []),
|
|
91
|
+
getTask: vi.fn(() => null),
|
|
92
|
+
listTasks: vi.fn(() => []),
|
|
93
|
+
getMessages: vi.fn(() => []),
|
|
94
|
+
getFullMessage: vi.fn(() => null),
|
|
95
|
+
addSubscription: vi.fn(),
|
|
96
|
+
removeSubscription: vi.fn(),
|
|
97
|
+
getSubscriptions: vi.fn(() => []),
|
|
98
|
+
getSubscribers: vi.fn(() => []),
|
|
99
|
+
onAgentChange: vi.fn(() => () => {}),
|
|
100
|
+
onTaskChange: vi.fn(() => () => {}),
|
|
101
|
+
onMessageChange: vi.fn(() => () => {}),
|
|
102
|
+
persist: vi.fn(async () => {}),
|
|
103
|
+
close: vi.fn(async () => {}),
|
|
104
|
+
updateAgentMetadata: vi.fn(),
|
|
105
|
+
} as unknown as EventStore;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createMockAgentManager(): AgentManager {
|
|
109
|
+
return {
|
|
110
|
+
spawn: vi.fn(async (options: any) => ({
|
|
111
|
+
id: "agent_spawned",
|
|
112
|
+
session_id: "sess_spawned",
|
|
113
|
+
agent: {
|
|
114
|
+
id: "agent_spawned",
|
|
115
|
+
session_id: "sess_spawned",
|
|
116
|
+
task: options.task,
|
|
117
|
+
task_id: "task_spawned",
|
|
118
|
+
state: "running",
|
|
119
|
+
parent: options.parent,
|
|
120
|
+
lineage: [],
|
|
121
|
+
config: {},
|
|
122
|
+
cwd: options.cwd ?? "/test",
|
|
123
|
+
created_at: Date.now(),
|
|
124
|
+
},
|
|
125
|
+
session: {} as any,
|
|
126
|
+
})),
|
|
127
|
+
terminate: vi.fn(async () => {}),
|
|
128
|
+
resume: vi.fn(async () => ({} as any)),
|
|
129
|
+
get: vi.fn(() => null),
|
|
130
|
+
list: vi.fn(() => []),
|
|
131
|
+
getChildren: vi.fn(() => []),
|
|
132
|
+
getHierarchy: vi.fn(() => null),
|
|
133
|
+
getOrCreateHeadManager: vi.fn(async () => ({} as any)),
|
|
134
|
+
listHeadManagers: vi.fn(() => []),
|
|
135
|
+
prompt: vi.fn(async function* () {}),
|
|
136
|
+
getSession: vi.fn(() => null),
|
|
137
|
+
hasActiveSession: vi.fn(() => false),
|
|
138
|
+
onLifecycleEvent: vi.fn(() => () => {}),
|
|
139
|
+
close: vi.fn(async () => {}),
|
|
140
|
+
} as unknown as AgentManager;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createMockTaskManager(): TaskManager {
|
|
144
|
+
return {
|
|
145
|
+
create: vi.fn(() => ({
|
|
146
|
+
id: "task_created",
|
|
147
|
+
description: "test",
|
|
148
|
+
status: "pending",
|
|
149
|
+
created_at: Date.now(),
|
|
150
|
+
created_by: "agent_test",
|
|
151
|
+
})),
|
|
152
|
+
get: vi.fn(() => null),
|
|
153
|
+
list: vi.fn(() => []),
|
|
154
|
+
getSubtasks: vi.fn(() => []),
|
|
155
|
+
getSubtaskStatus: vi.fn(() => ({ total: 0, pending: 0, completed: 0, failed: 0 })),
|
|
156
|
+
updateStatus: vi.fn(),
|
|
157
|
+
assign: vi.fn(),
|
|
158
|
+
} as unknown as TaskManager;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function createMockMessageRouter(): MessageRouter {
|
|
162
|
+
return {
|
|
163
|
+
sendToAddress: vi.fn(async () => ({
|
|
164
|
+
id: "msg_123",
|
|
165
|
+
delivered: [{ subscriber: "agent_target" }],
|
|
166
|
+
})),
|
|
167
|
+
getMessages: vi.fn(() => []),
|
|
168
|
+
} as unknown as MessageRouter;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createMockPeerManager(): PeerManager {
|
|
172
|
+
return {
|
|
173
|
+
hasTransport: vi.fn(() => true),
|
|
174
|
+
sendMessage: vi.fn(async () => {}),
|
|
175
|
+
sendRequest: vi.fn(async () => ({ result: "ok" })),
|
|
176
|
+
respondToRequest: vi.fn(),
|
|
177
|
+
getPeerMessages: vi.fn(() => []),
|
|
178
|
+
} as unknown as PeerManager;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function createMockActivityWatcher(): ActivityWatcher {
|
|
182
|
+
return {
|
|
183
|
+
isRunning: vi.fn(() => true),
|
|
184
|
+
start: vi.fn(),
|
|
185
|
+
stop: vi.fn(),
|
|
186
|
+
processActivity: vi.fn(),
|
|
187
|
+
subscribeAgent: vi.fn(),
|
|
188
|
+
unsubscribeAgent: vi.fn(),
|
|
189
|
+
} as unknown as ActivityWatcher;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createMockTaskBackend(): TaskBackend {
|
|
193
|
+
return {
|
|
194
|
+
list: vi.fn(async () => []),
|
|
195
|
+
get: vi.fn(async () => null),
|
|
196
|
+
create: vi.fn(async () => ({})),
|
|
197
|
+
update: vi.fn(async () => ({})),
|
|
198
|
+
claim: vi.fn(async () => ({})),
|
|
199
|
+
unclaim: vi.fn(async () => {}),
|
|
200
|
+
} as unknown as TaskBackend;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Valid agent context included in params.context.
|
|
205
|
+
*/
|
|
206
|
+
function validContext() {
|
|
207
|
+
return {
|
|
208
|
+
agent_id: "agent_caller",
|
|
209
|
+
session_id: "sess_caller",
|
|
210
|
+
task_id: "task_caller",
|
|
211
|
+
lineage: ["agent_root"],
|
|
212
|
+
cwd: "/test/cwd",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create params with valid context + additional args.
|
|
218
|
+
*/
|
|
219
|
+
function withContext(args: Record<string, unknown> = {}) {
|
|
220
|
+
return { ...args, context: validContext() };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// Tests
|
|
225
|
+
// =============================================================================
|
|
226
|
+
|
|
227
|
+
describe("MCP Bridge Extensions", () => {
|
|
228
|
+
let adapter: MAPAdapter & { handlers: Map<string, ExtensionHandler> };
|
|
229
|
+
let services: MCPBridgeServices;
|
|
230
|
+
let ctx: ExtensionContext;
|
|
231
|
+
|
|
232
|
+
beforeEach(() => {
|
|
233
|
+
adapter = createMockAdapter();
|
|
234
|
+
services = {
|
|
235
|
+
eventStore: createMockEventStore(),
|
|
236
|
+
agentManager: createMockAgentManager(),
|
|
237
|
+
taskManager: createMockTaskManager(),
|
|
238
|
+
messageRouter: createMockMessageRouter(),
|
|
239
|
+
};
|
|
240
|
+
ctx = createMockContext();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────
|
|
244
|
+
// Registration
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
describe("registration", () => {
|
|
248
|
+
it("registers 11 core methods without optional services", () => {
|
|
249
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
250
|
+
expect(adapter.handlers.size).toBe(11);
|
|
251
|
+
expect(adapter.handlers.has("_macro/mcp/spawn_agent")).toBe(true);
|
|
252
|
+
expect(adapter.handlers.has("_macro/mcp/emit_status")).toBe(true);
|
|
253
|
+
expect(adapter.handlers.has("_macro/mcp/send_message")).toBe(true);
|
|
254
|
+
expect(adapter.handlers.has("_macro/mcp/check_messages")).toBe(true);
|
|
255
|
+
expect(adapter.handlers.has("_macro/mcp/query_index")).toBe(true);
|
|
256
|
+
expect(adapter.handlers.has("_macro/mcp/get_hierarchy")).toBe(true);
|
|
257
|
+
expect(adapter.handlers.has("_macro/mcp/get_agent_summary")).toBe(true);
|
|
258
|
+
expect(adapter.handlers.has("_macro/mcp/stop_agent")).toBe(true);
|
|
259
|
+
expect(adapter.handlers.has("_macro/mcp/done")).toBe(true);
|
|
260
|
+
expect(adapter.handlers.has("_macro/mcp/inject_context")).toBe(true);
|
|
261
|
+
expect(adapter.handlers.has("_macro/mcp/task_tools_list")).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("registers wait_for_activity when activityWatcher is provided", () => {
|
|
265
|
+
services.activityWatcher = createMockActivityWatcher();
|
|
266
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
267
|
+
expect(adapter.handlers.has("_macro/mcp/wait_for_activity")).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("does not register wait_for_activity when activityWatcher is absent", () => {
|
|
271
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
272
|
+
expect(adapter.handlers.has("_macro/mcp/wait_for_activity")).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("registers task backend methods when taskBackend is provided", () => {
|
|
276
|
+
services.taskBackend = createMockTaskBackend();
|
|
277
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
278
|
+
expect(adapter.handlers.has("_macro/mcp/claim_task")).toBe(true);
|
|
279
|
+
expect(adapter.handlers.has("_macro/mcp/unclaim_task")).toBe(true);
|
|
280
|
+
expect(adapter.handlers.has("_macro/mcp/list_claimable_tasks")).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("does not register task backend methods when taskBackend is absent", () => {
|
|
284
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
285
|
+
expect(adapter.handlers.has("_macro/mcp/claim_task")).toBe(false);
|
|
286
|
+
expect(adapter.handlers.has("_macro/mcp/unclaim_task")).toBe(false);
|
|
287
|
+
expect(adapter.handlers.has("_macro/mcp/list_claimable_tasks")).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("registers peer methods when peerManager is provided", () => {
|
|
291
|
+
services.peerManager = createMockPeerManager();
|
|
292
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
293
|
+
expect(adapter.handlers.has("_macro/mcp/send_peer_message")).toBe(true);
|
|
294
|
+
expect(adapter.handlers.has("_macro/mcp/send_peer_request")).toBe(true);
|
|
295
|
+
expect(adapter.handlers.has("_macro/mcp/respond_to_peer_request")).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("does not register peer methods when peerManager is absent", () => {
|
|
299
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
300
|
+
expect(adapter.handlers.has("_macro/mcp/send_peer_message")).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("registers all 18 methods with all optional services", () => {
|
|
304
|
+
services.activityWatcher = createMockActivityWatcher();
|
|
305
|
+
services.taskBackend = createMockTaskBackend();
|
|
306
|
+
services.peerManager = createMockPeerManager();
|
|
307
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
308
|
+
expect(adapter.handlers.size).toBe(18);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("unregisters all methods via unregisterMCPBridgeExtensions", () => {
|
|
312
|
+
services.activityWatcher = createMockActivityWatcher();
|
|
313
|
+
services.taskBackend = createMockTaskBackend();
|
|
314
|
+
services.peerManager = createMockPeerManager();
|
|
315
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
316
|
+
expect(adapter.handlers.size).toBe(18);
|
|
317
|
+
|
|
318
|
+
unregisterMCPBridgeExtensions(adapter);
|
|
319
|
+
// Dynamic task tool methods (registered per-tool, not in MCP_BRIDGE_METHODS)
|
|
320
|
+
// are not cleaned up by unregisterMCPBridgeExtensions since their names are
|
|
321
|
+
// dynamic. Only the static MCP_BRIDGE_METHODS entries are removed.
|
|
322
|
+
expect(adapter.handlers.size).toBe(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("MCP_BRIDGE_METHODS lists all 18 static methods", () => {
|
|
326
|
+
expect(MCP_BRIDGE_METHODS).toHaveLength(18);
|
|
327
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/spawn_agent");
|
|
328
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/done");
|
|
329
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/wait_for_activity");
|
|
330
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/claim_task");
|
|
331
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/send_peer_message");
|
|
332
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/respond_to_peer_request");
|
|
333
|
+
expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/task_tools_list");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ─────────────────────────────────────────────────────────────────
|
|
338
|
+
// Context extraction
|
|
339
|
+
// ─────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
describe("context extraction", () => {
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("rejects when params.context is missing", async () => {
|
|
347
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
348
|
+
await expect(handler(ctx, { summary: "test" })).rejects.toThrow(
|
|
349
|
+
"params.context.agent_id is required"
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("rejects when params.context.agent_id is missing", async () => {
|
|
354
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
355
|
+
await expect(
|
|
356
|
+
handler(ctx, { context: { session_id: "s1" }, summary: "test" })
|
|
357
|
+
).rejects.toThrow("params.context.agent_id is required");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("rejects when params is null", async () => {
|
|
361
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
362
|
+
await expect(handler(ctx, null)).rejects.toThrow(
|
|
363
|
+
"params.context.agent_id is required"
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("separates context from args correctly", async () => {
|
|
368
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
369
|
+
await handler(ctx, withContext({ status_type: "checkpoint", summary: "half done" }));
|
|
370
|
+
|
|
371
|
+
expect(services.eventStore.emit).toHaveBeenCalledWith(
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
type: "status",
|
|
374
|
+
source: { agent_id: "agent_caller" },
|
|
375
|
+
payload: expect.objectContaining({
|
|
376
|
+
status_type: "checkpoint",
|
|
377
|
+
summary: "half done",
|
|
378
|
+
}),
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ─────────────────────────────────────────────────────────────────
|
|
385
|
+
// spawn_agent
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe("_macro/mcp/spawn_agent", () => {
|
|
389
|
+
beforeEach(() => {
|
|
390
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("calls agentManager.spawn with parent = context.agent_id", async () => {
|
|
394
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
395
|
+
await handler(ctx, withContext({ task: "child task" }));
|
|
396
|
+
|
|
397
|
+
expect(services.agentManager.spawn).toHaveBeenCalledWith(
|
|
398
|
+
expect.objectContaining({
|
|
399
|
+
task: "child task",
|
|
400
|
+
parent: "agent_caller",
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("returns agent_id, task_id, session_id", async () => {
|
|
406
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
407
|
+
const result = (await handler(ctx, withContext({ task: "test" }))) as any;
|
|
408
|
+
|
|
409
|
+
expect(result.agent_id).toBe("agent_spawned");
|
|
410
|
+
expect(result.task_id).toBe("task_spawned");
|
|
411
|
+
expect(result.session_id).toBe("sess_spawned");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("defaults subscribe_parent to true and topics to []", async () => {
|
|
415
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
416
|
+
await handler(ctx, withContext({ task: "test" }));
|
|
417
|
+
|
|
418
|
+
expect(services.agentManager.spawn).toHaveBeenCalledWith(
|
|
419
|
+
expect.objectContaining({
|
|
420
|
+
subscribeParent: true,
|
|
421
|
+
topics: [],
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ─────────────────────────────────────────────────────────────────
|
|
428
|
+
// emit_status
|
|
429
|
+
// ─────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
describe("_macro/mcp/emit_status", () => {
|
|
432
|
+
beforeEach(() => {
|
|
433
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("emits status event via eventStore.emit", async () => {
|
|
437
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
438
|
+
const result = (await handler(
|
|
439
|
+
ctx,
|
|
440
|
+
withContext({ status_type: "checkpoint", summary: "50% done" })
|
|
441
|
+
)) as any;
|
|
442
|
+
|
|
443
|
+
expect(services.eventStore.emit).toHaveBeenCalledWith(
|
|
444
|
+
expect.objectContaining({
|
|
445
|
+
type: "status",
|
|
446
|
+
source: { agent_id: "agent_caller" },
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
expect(result.event_id).toBeDefined();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("updates task status when complete_task is true and status is completed", async () => {
|
|
453
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
454
|
+
const result = (await handler(
|
|
455
|
+
ctx,
|
|
456
|
+
withContext({ status_type: "completed", summary: "done", complete_task: true })
|
|
457
|
+
)) as any;
|
|
458
|
+
|
|
459
|
+
expect(services.taskManager.updateStatus).toHaveBeenCalledWith("task_caller", "completed");
|
|
460
|
+
expect(result.task_updated).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("updates task status for failed with complete_task", async () => {
|
|
464
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
465
|
+
const result = (await handler(
|
|
466
|
+
ctx,
|
|
467
|
+
withContext({ status_type: "failed", summary: "error", complete_task: true })
|
|
468
|
+
)) as any;
|
|
469
|
+
|
|
470
|
+
expect(services.taskManager.updateStatus).toHaveBeenCalledWith("task_caller", "failed");
|
|
471
|
+
expect(result.task_updated).toBe(true);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("does not update task when complete_task is false", async () => {
|
|
475
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
476
|
+
const result = (await handler(
|
|
477
|
+
ctx,
|
|
478
|
+
withContext({ status_type: "completed", summary: "done", complete_task: false })
|
|
479
|
+
)) as any;
|
|
480
|
+
|
|
481
|
+
expect(services.taskManager.updateStatus).not.toHaveBeenCalled();
|
|
482
|
+
expect(result.task_updated).toBe(false);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("emits status_emitted MAP event for TUI subscribers", async () => {
|
|
486
|
+
const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
|
|
487
|
+
await handler(
|
|
488
|
+
ctx,
|
|
489
|
+
withContext({ status_type: "checkpoint", summary: "50% done", details: "halfway" })
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
expect(adapter.emitEvent).toHaveBeenCalledWith(
|
|
493
|
+
expect.objectContaining({
|
|
494
|
+
type: "status_emitted",
|
|
495
|
+
agentId: "agent_caller",
|
|
496
|
+
data: expect.objectContaining({
|
|
497
|
+
agentId: "agent_caller",
|
|
498
|
+
taskId: "task_caller",
|
|
499
|
+
statusType: "checkpoint",
|
|
500
|
+
summary: "50% done",
|
|
501
|
+
details: "halfway",
|
|
502
|
+
}),
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// ─────────────────────────────────────────────────────────────────
|
|
509
|
+
// send_message
|
|
510
|
+
// ─────────────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
describe("_macro/mcp/send_message", () => {
|
|
513
|
+
beforeEach(() => {
|
|
514
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("sends to agent by agent_id", async () => {
|
|
518
|
+
const handler = adapter.handlers.get("_macro/mcp/send_message")!;
|
|
519
|
+
await handler(
|
|
520
|
+
ctx,
|
|
521
|
+
withContext({ to: { agent_id: "agent_target" }, content: "hello" })
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
expect(services.messageRouter.sendToAddress).toHaveBeenCalledWith(
|
|
525
|
+
expect.objectContaining({
|
|
526
|
+
from: "agent_caller",
|
|
527
|
+
to: { agent: "agent_target" },
|
|
528
|
+
content: "hello",
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("sends to task by task_id", async () => {
|
|
534
|
+
const handler = adapter.handlers.get("_macro/mcp/send_message")!;
|
|
535
|
+
await handler(
|
|
536
|
+
ctx,
|
|
537
|
+
withContext({ to: { task_id: "task_target" }, content: "update" })
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(services.messageRouter.sendToAddress).toHaveBeenCalledWith(
|
|
541
|
+
expect.objectContaining({
|
|
542
|
+
to: { task: "task_target" },
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("throws when 'to' is missing", async () => {
|
|
548
|
+
const handler = adapter.handlers.get("_macro/mcp/send_message")!;
|
|
549
|
+
await expect(
|
|
550
|
+
handler(ctx, withContext({ content: "hello" }))
|
|
551
|
+
).rejects.toThrow("params.to is required");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("throws when no target specified in to", async () => {
|
|
555
|
+
const handler = adapter.handlers.get("_macro/mcp/send_message")!;
|
|
556
|
+
await expect(
|
|
557
|
+
handler(ctx, withContext({ to: {}, content: "hello" }))
|
|
558
|
+
).rejects.toThrow("Must specify one of");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("returns message_id and delivered_to count", async () => {
|
|
562
|
+
const handler = adapter.handlers.get("_macro/mcp/send_message")!;
|
|
563
|
+
const result = (await handler(
|
|
564
|
+
ctx,
|
|
565
|
+
withContext({ to: { agent_id: "agent_target" }, content: "hello" })
|
|
566
|
+
)) as any;
|
|
567
|
+
|
|
568
|
+
expect(result.message_id).toBe("msg_123");
|
|
569
|
+
expect(result.delivered_to).toBe(1);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// ─────────────────────────────────────────────────────────────────
|
|
574
|
+
// check_messages
|
|
575
|
+
// ─────────────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
describe("_macro/mcp/check_messages", () => {
|
|
578
|
+
beforeEach(() => {
|
|
579
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("returns formatted messages", async () => {
|
|
583
|
+
(services.messageRouter.getMessages as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
584
|
+
{
|
|
585
|
+
id: "msg_1",
|
|
586
|
+
from: { agent_id: "agent_sender" },
|
|
587
|
+
content: "hello",
|
|
588
|
+
timestamp: 1000,
|
|
589
|
+
truncated: false,
|
|
590
|
+
},
|
|
591
|
+
]);
|
|
592
|
+
|
|
593
|
+
const handler = adapter.handlers.get("_macro/mcp/check_messages")!;
|
|
594
|
+
const result = (await handler(ctx, withContext({ limit: 10 }))) as any;
|
|
595
|
+
|
|
596
|
+
expect(result.messages).toHaveLength(1);
|
|
597
|
+
expect(result.messages[0].from).toBe("agent:agent_sender");
|
|
598
|
+
expect(result.messages[0].content).toBe("hello");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("returns total_pending count", async () => {
|
|
602
|
+
(services.messageRouter.getMessages as ReturnType<typeof vi.fn>)
|
|
603
|
+
.mockReturnValueOnce([]) // limited query
|
|
604
|
+
.mockReturnValueOnce([{ id: "m1" }, { id: "m2" }]); // all query
|
|
605
|
+
|
|
606
|
+
const handler = adapter.handlers.get("_macro/mcp/check_messages")!;
|
|
607
|
+
const result = (await handler(ctx, withContext({}))) as any;
|
|
608
|
+
|
|
609
|
+
expect(result.total_pending).toBe(2);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// ─────────────────────────────────────────────────────────────────
|
|
614
|
+
// query_index
|
|
615
|
+
// ─────────────────────────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
describe("_macro/mcp/query_index", () => {
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("queries agents when type is 'agents'", async () => {
|
|
623
|
+
(services.agentManager.list as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
624
|
+
{ id: "agent_1", task: "Task 1", state: "running" },
|
|
625
|
+
{ id: "agent_2", task: "Task 2", state: "stopped" },
|
|
626
|
+
]);
|
|
627
|
+
|
|
628
|
+
const handler = adapter.handlers.get("_macro/mcp/query_index")!;
|
|
629
|
+
const result = (await handler(ctx, withContext({ type: "agents" }))) as any;
|
|
630
|
+
|
|
631
|
+
expect(result.entries).toHaveLength(2);
|
|
632
|
+
expect(result.entries[0].type).toBe("agent");
|
|
633
|
+
expect(result.total).toBe(2);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("queries tasks when type is 'tasks'", async () => {
|
|
637
|
+
(services.taskManager.list as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
638
|
+
{ id: "task_1", description: "Do thing", status: "pending" },
|
|
639
|
+
]);
|
|
640
|
+
|
|
641
|
+
const handler = adapter.handlers.get("_macro/mcp/query_index")!;
|
|
642
|
+
const result = (await handler(ctx, withContext({ type: "tasks" }))) as any;
|
|
643
|
+
|
|
644
|
+
expect(result.entries).toHaveLength(1);
|
|
645
|
+
expect(result.entries[0].type).toBe("task");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("applies state filter for agents", async () => {
|
|
649
|
+
(services.agentManager.list as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
650
|
+
{ id: "agent_1", task: "Task 1", state: "running" },
|
|
651
|
+
{ id: "agent_2", task: "Task 2", state: "stopped" },
|
|
652
|
+
]);
|
|
653
|
+
|
|
654
|
+
const handler = adapter.handlers.get("_macro/mcp/query_index")!;
|
|
655
|
+
const result = (await handler(
|
|
656
|
+
ctx,
|
|
657
|
+
withContext({ type: "agents", filter: { state: "running" } })
|
|
658
|
+
)) as any;
|
|
659
|
+
|
|
660
|
+
expect(result.entries).toHaveLength(1);
|
|
661
|
+
expect(result.entries[0].id).toBe("agent_1");
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("returns has_more flag with pagination", async () => {
|
|
665
|
+
const agents = Array.from({ length: 25 }, (_, i) => ({
|
|
666
|
+
id: `agent_${i}`,
|
|
667
|
+
task: `Task ${i}`,
|
|
668
|
+
state: "running",
|
|
669
|
+
}));
|
|
670
|
+
(services.agentManager.list as ReturnType<typeof vi.fn>).mockReturnValue(agents);
|
|
671
|
+
|
|
672
|
+
const handler = adapter.handlers.get("_macro/mcp/query_index")!;
|
|
673
|
+
const result = (await handler(
|
|
674
|
+
ctx,
|
|
675
|
+
withContext({ type: "agents", limit: 10 })
|
|
676
|
+
)) as any;
|
|
677
|
+
|
|
678
|
+
expect(result.entries).toHaveLength(10);
|
|
679
|
+
expect(result.total).toBe(25);
|
|
680
|
+
expect(result.has_more).toBe(true);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// ─────────────────────────────────────────────────────────────────
|
|
685
|
+
// get_hierarchy
|
|
686
|
+
// ─────────────────────────────────────────────────────────────────
|
|
687
|
+
|
|
688
|
+
describe("_macro/mcp/get_hierarchy", () => {
|
|
689
|
+
beforeEach(() => {
|
|
690
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("returns hierarchy tree for agent", async () => {
|
|
694
|
+
(services.agentManager.getHierarchy as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
695
|
+
root: {
|
|
696
|
+
agent: { id: "agent_root", task: "Root task", state: "running" },
|
|
697
|
+
children: [],
|
|
698
|
+
},
|
|
699
|
+
depth: 1,
|
|
700
|
+
totalAgents: 1,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const handler = adapter.handlers.get("_macro/mcp/get_hierarchy")!;
|
|
704
|
+
const result = (await handler(
|
|
705
|
+
ctx,
|
|
706
|
+
withContext({ root: "agent_root" })
|
|
707
|
+
)) as any;
|
|
708
|
+
|
|
709
|
+
expect(result.tree.agent_id).toBe("agent_root");
|
|
710
|
+
expect(result.depth).toBe(1);
|
|
711
|
+
expect(result.total_agents).toBe(1);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("throws notFound for non-existent agent", async () => {
|
|
715
|
+
const handler = adapter.handlers.get("_macro/mcp/get_hierarchy")!;
|
|
716
|
+
await expect(
|
|
717
|
+
handler(ctx, withContext({ root: "nonexistent" }))
|
|
718
|
+
).rejects.toThrow("not found");
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// ─────────────────────────────────────────────────────────────────
|
|
723
|
+
// get_agent_summary
|
|
724
|
+
// ─────────────────────────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
describe("_macro/mcp/get_agent_summary", () => {
|
|
727
|
+
beforeEach(() => {
|
|
728
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("returns agent details", async () => {
|
|
732
|
+
(services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
733
|
+
id: "agent_target",
|
|
734
|
+
session_id: "sess_target",
|
|
735
|
+
task: "Target task",
|
|
736
|
+
state: "running",
|
|
737
|
+
parent: "agent_root",
|
|
738
|
+
created_at: 1000,
|
|
739
|
+
});
|
|
740
|
+
(services.agentManager.getChildren as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
741
|
+
{ id: "child_1" },
|
|
742
|
+
]);
|
|
743
|
+
|
|
744
|
+
const handler = adapter.handlers.get("_macro/mcp/get_agent_summary")!;
|
|
745
|
+
const result = (await handler(
|
|
746
|
+
ctx,
|
|
747
|
+
withContext({ agent_id: "agent_target" })
|
|
748
|
+
)) as any;
|
|
749
|
+
|
|
750
|
+
expect(result.id).toBe("agent_target");
|
|
751
|
+
expect(result.task).toBe("Target task");
|
|
752
|
+
expect(result.children_count).toBe(1);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("throws notFound for non-existent agent", async () => {
|
|
756
|
+
const handler = adapter.handlers.get("_macro/mcp/get_agent_summary")!;
|
|
757
|
+
await expect(
|
|
758
|
+
handler(ctx, withContext({ agent_id: "nonexistent" }))
|
|
759
|
+
).rejects.toThrow("not found");
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// ─────────────────────────────────────────────────────────────────
|
|
764
|
+
// stop_agent
|
|
765
|
+
// ─────────────────────────────────────────────────────────────────
|
|
766
|
+
|
|
767
|
+
describe("_macro/mcp/stop_agent", () => {
|
|
768
|
+
beforeEach(() => {
|
|
769
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("terminates agent in caller's subtree", async () => {
|
|
773
|
+
(services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
774
|
+
id: "agent_child",
|
|
775
|
+
state: "running",
|
|
776
|
+
lineage: ["agent_root", "agent_caller"],
|
|
777
|
+
});
|
|
778
|
+
(services.agentManager.getChildren as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
|
779
|
+
|
|
780
|
+
const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
|
|
781
|
+
const result = (await handler(
|
|
782
|
+
ctx,
|
|
783
|
+
withContext({ agent_id: "agent_child" })
|
|
784
|
+
)) as any;
|
|
785
|
+
|
|
786
|
+
expect(services.agentManager.terminate).toHaveBeenCalledWith("agent_child", "cancelled");
|
|
787
|
+
expect(result.success).toBe(true);
|
|
788
|
+
expect(result.stopped_agents).toContain("agent_child");
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("throws permissionDenied for agent outside subtree", async () => {
|
|
792
|
+
(services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
793
|
+
id: "agent_other",
|
|
794
|
+
state: "running",
|
|
795
|
+
lineage: ["agent_root"],
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
|
|
799
|
+
await expect(
|
|
800
|
+
handler(ctx, withContext({ agent_id: "agent_other" }))
|
|
801
|
+
).rejects.toThrow("Cannot stop agent outside your subtree");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it("throws notFound for non-existent agent", async () => {
|
|
805
|
+
const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
|
|
806
|
+
await expect(
|
|
807
|
+
handler(ctx, withContext({ agent_id: "nonexistent" }))
|
|
808
|
+
).rejects.toThrow("not found");
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("allows stopping self", async () => {
|
|
812
|
+
(services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
813
|
+
id: "agent_caller",
|
|
814
|
+
state: "running",
|
|
815
|
+
lineage: ["agent_root"],
|
|
816
|
+
});
|
|
817
|
+
(services.agentManager.getChildren as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
|
818
|
+
|
|
819
|
+
const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
|
|
820
|
+
const result = (await handler(
|
|
821
|
+
ctx,
|
|
822
|
+
withContext({ agent_id: "agent_caller" })
|
|
823
|
+
)) as any;
|
|
824
|
+
|
|
825
|
+
expect(result.success).toBe(true);
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// ─────────────────────────────────────────────────────────────────
|
|
830
|
+
// done (delegates to createDoneHandler)
|
|
831
|
+
// ─────────────────────────────────────────────────────────────────
|
|
832
|
+
|
|
833
|
+
describe("_macro/mcp/done", () => {
|
|
834
|
+
beforeEach(() => {
|
|
835
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
836
|
+
// done handler needs the agent to exist for role resolution
|
|
837
|
+
(services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
838
|
+
id: "agent_caller",
|
|
839
|
+
state: "running",
|
|
840
|
+
role: "worker",
|
|
841
|
+
task_id: "task_caller",
|
|
842
|
+
parent: "agent_root",
|
|
843
|
+
lineage: ["agent_root"],
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("delegates to createDoneHandler and returns result", async () => {
|
|
848
|
+
const handler = adapter.handlers.get("_macro/mcp/done")!;
|
|
849
|
+
const result = (await handler(
|
|
850
|
+
ctx,
|
|
851
|
+
withContext({ status: "completed", summary: "all done" })
|
|
852
|
+
)) as any;
|
|
853
|
+
|
|
854
|
+
// The done handler should return a result with shouldTerminate
|
|
855
|
+
expect(result).toBeDefined();
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// ─────────────────────────────────────────────────────────────────
|
|
860
|
+
// spawn_agent — fire-and-forget prompt session streaming
|
|
861
|
+
// ─────────────────────────────────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
describe("spawn_agent session streaming", () => {
|
|
864
|
+
/**
|
|
865
|
+
* Helper: wait until adapter.emitEvent has been called with a specific event type.
|
|
866
|
+
* Polls every 10ms up to timeoutMs.
|
|
867
|
+
*/
|
|
868
|
+
async function waitForEmitEvent(
|
|
869
|
+
emitFn: ReturnType<typeof vi.fn>,
|
|
870
|
+
eventType: string,
|
|
871
|
+
timeoutMs = 5000,
|
|
872
|
+
): Promise<unknown> {
|
|
873
|
+
const start = Date.now();
|
|
874
|
+
while (Date.now() - start < timeoutMs) {
|
|
875
|
+
const call = emitFn.mock.calls.find(
|
|
876
|
+
(c: unknown[]) => (c[0] as Record<string, unknown>)?.type === eventType,
|
|
877
|
+
);
|
|
878
|
+
if (call) return call[0];
|
|
879
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
880
|
+
}
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Timeout waiting for emitEvent(${eventType}). ` +
|
|
883
|
+
`Received: ${emitFn.mock.calls.map((c: unknown[]) => (c[0] as Record<string, unknown>)?.type).join(", ")}`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
beforeEach(() => {
|
|
888
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("emits session_user_message MAP event before prompt starts", async () => {
|
|
892
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
893
|
+
await handler(ctx, withContext({ task: "do something" }));
|
|
894
|
+
|
|
895
|
+
// Fire-and-forget IIFE starts immediately — wait for it
|
|
896
|
+
const event = await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_user_message");
|
|
897
|
+
const evt = event as Record<string, unknown>;
|
|
898
|
+
expect(evt.type).toBe("session_user_message");
|
|
899
|
+
expect(evt.agentId).toBe("agent_spawned");
|
|
900
|
+
expect(evt.eventId).toBeDefined();
|
|
901
|
+
expect(evt.timestamp).toBeGreaterThan(0);
|
|
902
|
+
|
|
903
|
+
const data = evt.data as Record<string, unknown>;
|
|
904
|
+
expect(data.agentId).toBe("agent_spawned");
|
|
905
|
+
expect(data.sessionId).toBe("sess_spawned");
|
|
906
|
+
expect(data.content).toBe("do something");
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it("emits session_prompt_done MAP event after prompt completes", async () => {
|
|
910
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
911
|
+
await handler(ctx, withContext({ task: "finish this" }));
|
|
912
|
+
|
|
913
|
+
const event = await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
|
|
914
|
+
const evt = event as Record<string, unknown>;
|
|
915
|
+
expect(evt.type).toBe("session_prompt_done");
|
|
916
|
+
expect(evt.agentId).toBe("agent_spawned");
|
|
917
|
+
|
|
918
|
+
const data = evt.data as Record<string, unknown>;
|
|
919
|
+
expect(data.agentId).toBe("agent_spawned");
|
|
920
|
+
expect(data.stopReason).toBe("end_turn");
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("emits session_update MAP events for each prompt update", async () => {
|
|
924
|
+
// Configure prompt to yield controlled updates
|
|
925
|
+
const updates = [
|
|
926
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Hello" } },
|
|
927
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: " world" } },
|
|
928
|
+
{ sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read file", status: "running" },
|
|
929
|
+
{ sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read file", status: "completed", rawOutput: "contents" },
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
(services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
|
|
933
|
+
async function* () {
|
|
934
|
+
for (const u of updates) {
|
|
935
|
+
yield u;
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
941
|
+
await handler(ctx, withContext({ task: "read a file" }));
|
|
942
|
+
|
|
943
|
+
// Wait for the final event
|
|
944
|
+
await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
|
|
945
|
+
|
|
946
|
+
// Collect all emitEvent calls
|
|
947
|
+
const calls = (adapter.emitEvent as ReturnType<typeof vi.fn>).mock.calls.map(
|
|
948
|
+
(c: unknown[]) => c[0] as Record<string, unknown>,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
// Should have: 1 user_message + 4 session_updates + 1 prompt_done = 6 events
|
|
952
|
+
const userMessages = calls.filter((c) => c.type === "session_user_message");
|
|
953
|
+
const sessionUpdates = calls.filter((c) => c.type === "session_update");
|
|
954
|
+
const promptDones = calls.filter((c) => c.type === "session_prompt_done");
|
|
955
|
+
|
|
956
|
+
expect(userMessages).toHaveLength(1);
|
|
957
|
+
expect(sessionUpdates).toHaveLength(4);
|
|
958
|
+
expect(promptDones).toHaveLength(1);
|
|
959
|
+
|
|
960
|
+
// Verify update payloads contain the original update objects
|
|
961
|
+
const updatePayloads = sessionUpdates.map(
|
|
962
|
+
(c) => (c.data as Record<string, unknown>).update,
|
|
963
|
+
);
|
|
964
|
+
expect((updatePayloads[0] as Record<string, unknown>).sessionUpdate).toBe("agent_message_chunk");
|
|
965
|
+
expect((updatePayloads[2] as Record<string, unknown>).sessionUpdate).toBe("tool_call");
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("records user and assistant turns in eventStore after prompt completes", async () => {
|
|
969
|
+
const updates = [
|
|
970
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "I will help." } },
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
(services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
|
|
974
|
+
async function* () {
|
|
975
|
+
for (const u of updates) yield u;
|
|
976
|
+
},
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
980
|
+
await handler(ctx, withContext({ task: "help me" }));
|
|
981
|
+
|
|
982
|
+
// Wait for prompt to finish
|
|
983
|
+
await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
|
|
984
|
+
|
|
985
|
+
// Small delay for turn recording (happens after emitMAPEvent)
|
|
986
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
987
|
+
|
|
988
|
+
// Check eventStore.emit calls for turn recording
|
|
989
|
+
const turnCalls = (services.eventStore.emit as ReturnType<typeof vi.fn>).mock.calls.filter(
|
|
990
|
+
(c: unknown[]) => (c[0] as Record<string, unknown>)?.type === "turn",
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
expect(turnCalls.length).toBe(2);
|
|
994
|
+
|
|
995
|
+
// First turn: user prompt
|
|
996
|
+
const userTurn = turnCalls[0][0] as Record<string, unknown>;
|
|
997
|
+
expect(userTurn.type).toBe("turn");
|
|
998
|
+
expect((userTurn.source as Record<string, unknown>).agent_id).toBe("agent_spawned");
|
|
999
|
+
const userPayload = userTurn.payload as Record<string, unknown>;
|
|
1000
|
+
expect(userPayload.participant).toBe("user");
|
|
1001
|
+
expect(userPayload.content).toBe("help me");
|
|
1002
|
+
expect(userPayload.conversation_id).toBe("sess_spawned");
|
|
1003
|
+
|
|
1004
|
+
// Second turn: assistant response
|
|
1005
|
+
const assistantTurn = turnCalls[1][0] as Record<string, unknown>;
|
|
1006
|
+
const assistantPayload = assistantTurn.payload as Record<string, unknown>;
|
|
1007
|
+
expect(assistantPayload.participant).toBe("agent_spawned");
|
|
1008
|
+
expect(assistantPayload.content_type).toBe("assistant_response");
|
|
1009
|
+
const content = assistantPayload.content as { parts: Array<{ type: string; text?: string }> };
|
|
1010
|
+
expect(content.parts[0].type).toBe("text");
|
|
1011
|
+
expect(content.parts[0].text).toBe("I will help.");
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it("accumulates tool calls in assistant turn content", async () => {
|
|
1015
|
+
const updates = [
|
|
1016
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Reading file..." } },
|
|
1017
|
+
{ sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read", status: "running", _meta: { claudeCode: { toolName: "Read" } }, rawInput: { path: "/foo" } },
|
|
1018
|
+
{ sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read", status: "completed", rawOutput: "file contents", _meta: { claudeCode: { toolName: "Read" } }, rawInput: { path: "/foo" } },
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
(services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
|
|
1022
|
+
async function* () {
|
|
1023
|
+
for (const u of updates) yield u;
|
|
1024
|
+
},
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
1028
|
+
await handler(ctx, withContext({ task: "read /foo" }));
|
|
1029
|
+
|
|
1030
|
+
await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
|
|
1031
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1032
|
+
|
|
1033
|
+
const turnCalls = (services.eventStore.emit as ReturnType<typeof vi.fn>).mock.calls.filter(
|
|
1034
|
+
(c: unknown[]) => (c[0] as Record<string, unknown>)?.type === "turn",
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
const assistantTurn = turnCalls[1][0] as Record<string, unknown>;
|
|
1038
|
+
const assistantPayload = assistantTurn.payload as Record<string, unknown>;
|
|
1039
|
+
const content = assistantPayload.content as { parts: Array<Record<string, unknown>> };
|
|
1040
|
+
|
|
1041
|
+
// Should have text part + tool part
|
|
1042
|
+
expect(content.parts).toHaveLength(2);
|
|
1043
|
+
expect(content.parts[0].type).toBe("text");
|
|
1044
|
+
expect(content.parts[0].text).toBe("Reading file...");
|
|
1045
|
+
expect(content.parts[1].type).toBe("tool");
|
|
1046
|
+
expect(content.parts[1].toolCallId).toBe("tc_1");
|
|
1047
|
+
expect(content.parts[1].name).toBe("Read");
|
|
1048
|
+
expect(content.parts[1].output).toBe("file contents");
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it("emits session_prompt_done with stopReason 'error' when prompt throws", async () => {
|
|
1052
|
+
(services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
|
|
1053
|
+
async function* () {
|
|
1054
|
+
throw new Error("Agent process crashed");
|
|
1055
|
+
},
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
1059
|
+
await handler(ctx, withContext({ task: "crash test" }));
|
|
1060
|
+
|
|
1061
|
+
const event = await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
|
|
1062
|
+
const evt = event as Record<string, unknown>;
|
|
1063
|
+
const data = evt.data as Record<string, unknown>;
|
|
1064
|
+
expect(data.stopReason).toBe("error");
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it("does not emit session events when task is empty", async () => {
|
|
1068
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
1069
|
+
await handler(ctx, withContext({ task: "" }));
|
|
1070
|
+
|
|
1071
|
+
// Wait a bit to ensure no events are emitted
|
|
1072
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1073
|
+
|
|
1074
|
+
expect(adapter.emitEvent).not.toHaveBeenCalled();
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("all events include agentId at the top level for subscription routing", async () => {
|
|
1078
|
+
const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
|
|
1079
|
+
await handler(ctx, withContext({ task: "routing test" }));
|
|
1080
|
+
|
|
1081
|
+
await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
|
|
1082
|
+
|
|
1083
|
+
const calls = (adapter.emitEvent as ReturnType<typeof vi.fn>).mock.calls.map(
|
|
1084
|
+
(c: unknown[]) => c[0] as Record<string, unknown>,
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
for (const evt of calls) {
|
|
1088
|
+
expect(evt.agentId).toBe("agent_spawned");
|
|
1089
|
+
expect(evt.eventId).toBeDefined();
|
|
1090
|
+
expect(evt.timestamp).toBeGreaterThan(0);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1096
|
+
// Optional handler: activityWatcher absent
|
|
1097
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1098
|
+
|
|
1099
|
+
describe("optional handlers - activityWatcher absent", () => {
|
|
1100
|
+
it("wait_for_activity is not registered when absent", () => {
|
|
1101
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
1102
|
+
expect(adapter.handlers.has("_macro/mcp/wait_for_activity")).toBe(false);
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1107
|
+
// Optional handler: taskBackend absent
|
|
1108
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1109
|
+
|
|
1110
|
+
describe("optional handlers - taskBackend absent", () => {
|
|
1111
|
+
it("claim_task is not registered when absent", () => {
|
|
1112
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
1113
|
+
expect(adapter.handlers.has("_macro/mcp/claim_task")).toBe(false);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it("unclaim_task is not registered when absent", () => {
|
|
1117
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
1118
|
+
expect(adapter.handlers.has("_macro/mcp/unclaim_task")).toBe(false);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("list_claimable_tasks is not registered when absent", () => {
|
|
1122
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
1123
|
+
expect(adapter.handlers.has("_macro/mcp/list_claimable_tasks")).toBe(false);
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1128
|
+
// Optional handler: peerManager absent
|
|
1129
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1130
|
+
|
|
1131
|
+
describe("optional handlers - peerManager absent", () => {
|
|
1132
|
+
it("send_peer_message is not registered when absent", () => {
|
|
1133
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
1134
|
+
expect(adapter.handlers.has("_macro/mcp/send_peer_message")).toBe(false);
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1139
|
+
// Peer bridges (with peerManager)
|
|
1140
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1141
|
+
|
|
1142
|
+
describe("peer bridges", () => {
|
|
1143
|
+
beforeEach(() => {
|
|
1144
|
+
services.peerManager = createMockPeerManager();
|
|
1145
|
+
registerMCPBridgeExtensions(adapter, services);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it("send_peer_message sends via peerManager", async () => {
|
|
1149
|
+
const handler = adapter.handlers.get("_macro/mcp/send_peer_message")!;
|
|
1150
|
+
await handler(
|
|
1151
|
+
ctx,
|
|
1152
|
+
withContext({ to: "peer_target", type: "status", payload: { data: 1 } })
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
expect(services.peerManager!.sendMessage).toHaveBeenCalledWith(
|
|
1156
|
+
"agent_caller",
|
|
1157
|
+
"peer_target",
|
|
1158
|
+
expect.objectContaining({ type: "status", payload: { data: 1 } })
|
|
1159
|
+
);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("send_peer_request sends via peerManager and returns result", async () => {
|
|
1163
|
+
const handler = adapter.handlers.get("_macro/mcp/send_peer_request")!;
|
|
1164
|
+
const result = await handler(
|
|
1165
|
+
ctx,
|
|
1166
|
+
withContext({ to: "peer_target", method: "getStatus", params: {} })
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
expect(services.peerManager!.sendRequest).toHaveBeenCalled();
|
|
1170
|
+
expect(result).toEqual({ result: "ok" });
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
it("respond_to_peer_request calls peerManager.respondToRequest", async () => {
|
|
1174
|
+
const handler = adapter.handlers.get("_macro/mcp/respond_to_peer_request")!;
|
|
1175
|
+
await handler(
|
|
1176
|
+
ctx,
|
|
1177
|
+
withContext({ request_id: "req_1", result: { data: 42 } })
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
expect(services.peerManager!.respondToRequest).toHaveBeenCalledWith(
|
|
1181
|
+
"agent_caller",
|
|
1182
|
+
"req_1",
|
|
1183
|
+
expect.objectContaining({ result: { data: 42 } })
|
|
1184
|
+
);
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
});
|