macro-agent 0.1.8 → 0.1.10
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.md +166 -33
- package/README.md +781 -131
- package/dist/acp/claude-code-replay.d.ts +11 -0
- package/dist/acp/claude-code-replay.d.ts.map +1 -0
- package/dist/acp/claude-code-replay.js +190 -0
- package/dist/acp/claude-code-replay.js.map +1 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +155 -6
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +9 -0
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts +21 -0
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +234 -43
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +12 -0
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +15 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +41 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +16 -1
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/index.js +56 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
- package/dist/cognitive/macro-agent-backend.js +40 -22
- package/dist/cognitive/macro-agent-backend.js.map +1 -1
- package/dist/integrations/skilltree.d.ts.map +1 -1
- package/dist/integrations/skilltree.js +1 -0
- package/dist/integrations/skilltree.js.map +1 -1
- package/dist/lifecycle/cleanup.d.ts +33 -2
- package/dist/lifecycle/cleanup.d.ts.map +1 -1
- package/dist/lifecycle/cleanup.js +28 -6
- package/dist/lifecycle/cleanup.js.map +1 -1
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +28 -2
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/lifecycle/types.d.ts +11 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/map/acp-bridge.d.ts +9 -0
- package/dist/map/acp-bridge.d.ts.map +1 -1
- package/dist/map/acp-bridge.js +15 -2
- package/dist/map/acp-bridge.js.map +1 -1
- package/dist/map/cascade-bridge.d.ts +44 -0
- package/dist/map/cascade-bridge.d.ts.map +1 -0
- package/dist/map/cascade-bridge.js +257 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/lifecycle-bridge.d.ts +1 -1
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +58 -23
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +47 -6
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +33 -2
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +20 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +8 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-manager-v2.d.ts.map +1 -1
- package/dist/teams/team-manager-v2.js +26 -0
- package/dist/teams/team-manager-v2.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +16 -3
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/dist/workspace/config.d.ts +10 -10
- package/dist/workspace/config.d.ts.map +1 -1
- package/dist/workspace/config.js +4 -4
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/git-cascade-adapter.d.ts +510 -0
- package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
- package/dist/workspace/git-cascade-adapter.js +908 -0
- package/dist/workspace/git-cascade-adapter.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +4 -4
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/landing/direct-push.d.ts +20 -0
- package/dist/workspace/landing/direct-push.d.ts.map +1 -0
- package/dist/workspace/landing/direct-push.js +74 -0
- package/dist/workspace/landing/direct-push.js.map +1 -0
- package/dist/workspace/landing/index.d.ts +29 -0
- package/dist/workspace/landing/index.d.ts.map +1 -0
- package/dist/workspace/landing/index.js +37 -0
- package/dist/workspace/landing/index.js.map +1 -0
- package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
- package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
- package/dist/workspace/landing/merge-to-parent.js +185 -0
- package/dist/workspace/landing/merge-to-parent.js.map +1 -0
- package/dist/workspace/landing/optimistic-push.d.ts +16 -0
- package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
- package/dist/workspace/landing/optimistic-push.js +27 -0
- package/dist/workspace/landing/optimistic-push.js.map +1 -0
- package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
- package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
- package/dist/workspace/landing/queue-to-branch.js +79 -0
- package/dist/workspace/landing/queue-to-branch.js.map +1 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
- package/dist/workspace/merge-queue/merge-queue.js +10 -0
- package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
- package/dist/workspace/merge-queue/types.d.ts +16 -2
- package/dist/workspace/merge-queue/types.d.ts.map +1 -1
- package/dist/workspace/merge-queue/types.js +9 -0
- package/dist/workspace/merge-queue/types.js.map +1 -1
- package/dist/workspace/pool/types.d.ts +1 -0
- package/dist/workspace/pool/types.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.js +1 -0
- package/dist/workspace/pool/worktree-pool.js.map +1 -1
- package/dist/workspace/recovery/abandon.d.ts +15 -0
- package/dist/workspace/recovery/abandon.d.ts.map +1 -0
- package/dist/workspace/recovery/abandon.js +45 -0
- package/dist/workspace/recovery/abandon.js.map +1 -0
- package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
- package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
- package/dist/workspace/recovery/auto-resolve.js +99 -0
- package/dist/workspace/recovery/auto-resolve.js.map +1 -0
- package/dist/workspace/recovery/defer.d.ts +15 -0
- package/dist/workspace/recovery/defer.d.ts.map +1 -0
- package/dist/workspace/recovery/defer.js +16 -0
- package/dist/workspace/recovery/defer.js.map +1 -0
- package/dist/workspace/recovery/escalate.d.ts +16 -0
- package/dist/workspace/recovery/escalate.d.ts.map +1 -0
- package/dist/workspace/recovery/escalate.js +24 -0
- package/dist/workspace/recovery/escalate.js.map +1 -0
- package/dist/workspace/recovery/index.d.ts +32 -0
- package/dist/workspace/recovery/index.d.ts.map +1 -0
- package/dist/workspace/recovery/index.js +45 -0
- package/dist/workspace/recovery/index.js.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.js +111 -0
- package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
- package/dist/workspace/recovery/types.d.ts +63 -0
- package/dist/workspace/recovery/types.d.ts.map +1 -0
- package/dist/workspace/recovery/types.js +12 -0
- package/dist/workspace/recovery/types.js.map +1 -0
- package/dist/workspace/topology/index.d.ts +9 -0
- package/dist/workspace/topology/index.d.ts.map +1 -0
- package/dist/workspace/topology/index.js +8 -0
- package/dist/workspace/topology/index.js.map +1 -0
- package/dist/workspace/topology/no-workspace.d.ts +18 -0
- package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
- package/dist/workspace/topology/no-workspace.js +25 -0
- package/dist/workspace/topology/no-workspace.js.map +1 -0
- package/dist/workspace/topology/types.d.ts +97 -0
- package/dist/workspace/topology/types.d.ts.map +1 -0
- package/dist/workspace/topology/types.js +20 -0
- package/dist/workspace/topology/types.js.map +1 -0
- package/dist/workspace/topology/yaml-driven.d.ts +69 -0
- package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
- package/dist/workspace/topology/yaml-driven.js +273 -0
- package/dist/workspace/topology/yaml-driven.js.map +1 -0
- package/dist/workspace/types-v3.d.ts +110 -0
- package/dist/workspace/types-v3.d.ts.map +1 -0
- package/dist/workspace/types-v3.js +20 -0
- package/dist/workspace/types-v3.js.map +1 -0
- package/dist/workspace/types.d.ts +145 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +92 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +373 -13
- package/dist/workspace/workspace-manager.js.map +1 -1
- package/dist/workspace/yaml-schema.d.ts +254 -0
- package/dist/workspace/yaml-schema.d.ts.map +1 -0
- package/dist/workspace/yaml-schema.js +170 -0
- package/dist/workspace/yaml-schema.js.map +1 -0
- package/docs/conflict-recovery.md +472 -0
- package/docs/git-cascade-integration-gaps.md +678 -0
- package/docs/workspace-interfaces.md +731 -0
- package/docs/workspace-redesign-plan.md +302 -0
- package/package.json +4 -4
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
- package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
- package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
- package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
- package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
- package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
- package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
- package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
- package/src/acp/__tests__/macro-agent.test.ts +39 -1
- package/src/acp/claude-code-replay.ts +208 -0
- package/src/acp/macro-agent.ts +167 -9
- package/src/acp/types.ts +10 -0
- package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
- package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +293 -48
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +68 -1
- package/src/cli/index.ts +61 -0
- package/src/cognitive/macro-agent-backend.ts +45 -29
- package/src/integrations/skilltree.ts +1 -0
- package/src/lifecycle/cleanup.ts +52 -3
- package/src/lifecycle/handlers-v2.ts +40 -3
- package/src/lifecycle/types.ts +12 -0
- package/src/map/__tests__/cascade-bridge.test.ts +229 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-bridge.ts +301 -0
- package/src/map/lifecycle-bridge.ts +52 -17
- package/src/map/server.ts +47 -6
- package/src/map/sidecar.ts +31 -1
- package/src/map/types.ts +20 -0
- package/src/mcp/tools/done-v2.ts +9 -0
- package/src/teams/team-manager-v2.ts +37 -0
- package/src/teams/team-runtime-v2.ts +23 -3
- package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
- package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
- package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
- package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
- package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
- package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
- package/src/workspace/config.ts +11 -11
- package/src/workspace/git-cascade-adapter.ts +1186 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
- package/src/workspace/landing/direct-push.ts +91 -0
- package/src/workspace/landing/index.ts +40 -0
- package/src/workspace/landing/merge-to-parent.ts +228 -0
- package/src/workspace/landing/optimistic-push.ts +36 -0
- package/src/workspace/landing/queue-to-branch.ts +108 -0
- package/src/workspace/merge-queue/merge-queue.ts +10 -0
- package/src/workspace/merge-queue/types.ts +16 -2
- package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
- package/src/workspace/pool/types.ts +1 -0
- package/src/workspace/pool/worktree-pool.ts +1 -0
- package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
- package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
- package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
- package/src/workspace/recovery/abandon.ts +51 -0
- package/src/workspace/recovery/auto-resolve.ts +119 -0
- package/src/workspace/recovery/defer.ts +23 -0
- package/src/workspace/recovery/escalate.ts +30 -0
- package/src/workspace/recovery/index.ts +58 -0
- package/src/workspace/recovery/spawn-resolver.ts +145 -0
- package/src/workspace/recovery/types.ts +54 -0
- package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
- package/src/workspace/topology/index.ts +18 -0
- package/src/workspace/topology/no-workspace.ts +39 -0
- package/src/workspace/topology/types.ts +116 -0
- package/src/workspace/topology/yaml-driven.ts +316 -0
- package/src/workspace/types-v3.ts +155 -0
- package/src/workspace/types.ts +191 -20
- package/src/workspace/workspace-manager.ts +474 -19
- package/src/workspace/yaml-schema.ts +216 -0
- package/src/workspace/dataplane-adapter.ts +0 -546
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for createCascadeBridge.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the GitCascadeEvent → x-cascade/* MAP call translation directly
|
|
5
|
+
* via a mock adapter + mock connection, avoiding real git operations. E2E
|
|
6
|
+
* coverage lives elsewhere (workspace-v3 tests + OpenHive hub round-trips).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
10
|
+
import { createCascadeBridge } from '../cascade-bridge.js';
|
|
11
|
+
import type {
|
|
12
|
+
GitCascadeEvent,
|
|
13
|
+
GitCascadeEventCallback,
|
|
14
|
+
GitCascadeAdapter,
|
|
15
|
+
} from '../../workspace/git-cascade-adapter.js';
|
|
16
|
+
import type { LifecycleBridgeConnection } from '../lifecycle-bridge.js';
|
|
17
|
+
|
|
18
|
+
interface MockConnection extends LifecycleBridgeConnection {
|
|
19
|
+
calls: Array<{ method: string; params: unknown }>;
|
|
20
|
+
connected: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createMockConnection(connected = true): MockConnection {
|
|
24
|
+
const calls: Array<{ method: string; params: unknown }> = [];
|
|
25
|
+
return {
|
|
26
|
+
calls,
|
|
27
|
+
connected,
|
|
28
|
+
get isConnected() {
|
|
29
|
+
return this.connected;
|
|
30
|
+
},
|
|
31
|
+
async callExtension(method: string, params?: unknown): Promise<unknown> {
|
|
32
|
+
this.calls.push({ method, params });
|
|
33
|
+
return { ok: true };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface MockAdapter {
|
|
39
|
+
emit: (event: GitCascadeEvent) => void;
|
|
40
|
+
onEvent(callback: GitCascadeEventCallback): () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createMockAdapter(): MockAdapter {
|
|
44
|
+
const listeners = new Set<GitCascadeEventCallback>();
|
|
45
|
+
return {
|
|
46
|
+
emit(event: GitCascadeEvent) {
|
|
47
|
+
for (const listener of listeners) {
|
|
48
|
+
listener(event);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
onEvent(callback: GitCascadeEventCallback): () => void {
|
|
52
|
+
listeners.add(callback);
|
|
53
|
+
return () => listeners.delete(callback);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mkEvent<T extends GitCascadeEvent['type']>(
|
|
59
|
+
type: T,
|
|
60
|
+
data: Record<string, unknown>
|
|
61
|
+
): GitCascadeEvent {
|
|
62
|
+
return { type, timestamp: Date.now(), data } as GitCascadeEvent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('createCascadeBridge', () => {
|
|
66
|
+
let connection: MockConnection;
|
|
67
|
+
let adapter: MockAdapter;
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
connection = createMockConnection();
|
|
71
|
+
adapter = createMockAdapter();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
async function flushMicrotasks() {
|
|
75
|
+
// The bridge's callExtension is fire-and-forget (void + .catch). Yield
|
|
76
|
+
// to the microtask queue so the promise settles before assertions.
|
|
77
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('forwards stream:created as x-cascade/stream.opened with snake_case params', async () => {
|
|
81
|
+
createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
82
|
+
adapter.emit(
|
|
83
|
+
mkEvent('stream:created', {
|
|
84
|
+
streamId: 's1',
|
|
85
|
+
name: 'feat',
|
|
86
|
+
agentId: 'a1',
|
|
87
|
+
baseCommit: 'base',
|
|
88
|
+
branchName: 'stream/s1',
|
|
89
|
+
metadata: { task_ref: { resource_id: 'r', node_id: 'n' } },
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
await flushMicrotasks();
|
|
93
|
+
|
|
94
|
+
expect(connection.calls).toHaveLength(1);
|
|
95
|
+
expect(connection.calls[0].method).toBe('x-cascade/stream.opened');
|
|
96
|
+
expect(connection.calls[0].params).toMatchObject({
|
|
97
|
+
stream_id: 's1',
|
|
98
|
+
name: 'feat',
|
|
99
|
+
agent_id: 'a1',
|
|
100
|
+
base_commit: 'base',
|
|
101
|
+
branch_name: 'stream/s1',
|
|
102
|
+
metadata: { task_ref: { resource_id: 'r', node_id: 'n' } },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('forwards stream:committed with commit_hash and files_touched', async () => {
|
|
107
|
+
createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
108
|
+
adapter.emit(
|
|
109
|
+
mkEvent('stream:committed', {
|
|
110
|
+
streamId: 's1',
|
|
111
|
+
commit: 'abc',
|
|
112
|
+
changeId: 'c-123',
|
|
113
|
+
agentId: 'a',
|
|
114
|
+
messageSummary: 'feat: x',
|
|
115
|
+
filesTouched: ['a.ts', 'b.ts'],
|
|
116
|
+
parentCommit: 'base',
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
await flushMicrotasks();
|
|
120
|
+
|
|
121
|
+
expect(connection.calls[0].method).toBe('x-cascade/stream.committed');
|
|
122
|
+
expect(connection.calls[0].params).toMatchObject({
|
|
123
|
+
stream_id: 's1',
|
|
124
|
+
commit_hash: 'abc',
|
|
125
|
+
change_id: 'c-123',
|
|
126
|
+
files_touched: ['a.ts', 'b.ts'],
|
|
127
|
+
parent_commit: 'base',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('forwards cascade:rebased with new_commits array', async () => {
|
|
132
|
+
createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
133
|
+
adapter.emit(
|
|
134
|
+
mkEvent('cascade:rebased', {
|
|
135
|
+
streamId: 'dep',
|
|
136
|
+
agentId: 'a',
|
|
137
|
+
triggeredByStreamId: 'root',
|
|
138
|
+
triggeredByAgentId: 'a',
|
|
139
|
+
newBaseCommit: 'nb',
|
|
140
|
+
newHead: 'nh',
|
|
141
|
+
newCommits: [
|
|
142
|
+
{
|
|
143
|
+
commit_hash: 'r1',
|
|
144
|
+
change_id: 'chg',
|
|
145
|
+
parent_commit: 'nb',
|
|
146
|
+
message_summary: 'rebased',
|
|
147
|
+
files_touched: ['x.ts'],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
await flushMicrotasks();
|
|
153
|
+
|
|
154
|
+
expect(connection.calls[0].method).toBe('x-cascade/cascade.rebased');
|
|
155
|
+
expect(connection.calls[0].params).toMatchObject({
|
|
156
|
+
stream_id: 'dep',
|
|
157
|
+
triggered_by_stream_id: 'root',
|
|
158
|
+
new_base_commit: 'nb',
|
|
159
|
+
new_head: 'nh',
|
|
160
|
+
});
|
|
161
|
+
expect((connection.calls[0].params as { new_commits: unknown[] }).new_commits).toHaveLength(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('forwards cascade:completed with summary fields', async () => {
|
|
165
|
+
createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
166
|
+
adapter.emit(
|
|
167
|
+
mkEvent('cascade:completed', {
|
|
168
|
+
rootStreamId: 'root',
|
|
169
|
+
agentId: 'a',
|
|
170
|
+
strategy: 'stop_on_conflict',
|
|
171
|
+
updatedStreams: ['d1'],
|
|
172
|
+
failedStreams: [],
|
|
173
|
+
skippedStreams: [],
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
await flushMicrotasks();
|
|
177
|
+
|
|
178
|
+
expect(connection.calls[0].method).toBe('x-cascade/cascade.completed');
|
|
179
|
+
expect(connection.calls[0].params).toMatchObject({
|
|
180
|
+
root_stream_id: 'root',
|
|
181
|
+
strategy: 'stop_on_conflict',
|
|
182
|
+
updated_streams: ['d1'],
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('drops events while disconnected (standalone mode)', async () => {
|
|
187
|
+
connection.connected = false;
|
|
188
|
+
createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
189
|
+
adapter.emit(mkEvent('stream:created', { streamId: 's', name: 'n', agentId: 'a' }));
|
|
190
|
+
await flushMicrotasks();
|
|
191
|
+
expect(connection.calls).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('ignores events that have no MAP counterpart (e.g. worktree:created, mergeQueue:added)', async () => {
|
|
195
|
+
createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
196
|
+
adapter.emit(mkEvent('worktree:created', { agentId: 'a' }));
|
|
197
|
+
adapter.emit(mkEvent('mergeQueue:added', { streamId: 's' }));
|
|
198
|
+
adapter.emit(mkEvent('task:started', { taskId: 't' }));
|
|
199
|
+
await flushMicrotasks();
|
|
200
|
+
expect(connection.calls).toHaveLength(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('dispose() unsubscribes — no more forwarding after call', async () => {
|
|
204
|
+
const disposable = createCascadeBridge(connection, adapter as unknown as GitCascadeAdapter);
|
|
205
|
+
adapter.emit(mkEvent('stream:created', { streamId: 's1', name: 'x', agentId: 'a' }));
|
|
206
|
+
await flushMicrotasks();
|
|
207
|
+
expect(connection.calls).toHaveLength(1);
|
|
208
|
+
|
|
209
|
+
disposable.dispose();
|
|
210
|
+
|
|
211
|
+
adapter.emit(mkEvent('stream:created', { streamId: 's2', name: 'y', agentId: 'a' }));
|
|
212
|
+
await flushMicrotasks();
|
|
213
|
+
expect(connection.calls).toHaveLength(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('swallows callExtension errors (does not throw or block the event loop)', async () => {
|
|
217
|
+
const errConnection = createMockConnection();
|
|
218
|
+
errConnection.callExtension = vi.fn().mockRejectedValue(new Error('hub broke'));
|
|
219
|
+
createCascadeBridge(errConnection, adapter as unknown as GitCascadeAdapter);
|
|
220
|
+
|
|
221
|
+
expect(() =>
|
|
222
|
+
adapter.emit(
|
|
223
|
+
mkEvent('stream:abandoned', { streamId: 's', reason: 'testing' })
|
|
224
|
+
)
|
|
225
|
+
).not.toThrow();
|
|
226
|
+
await flushMicrotasks();
|
|
227
|
+
expect(errConnection.callExtension).toHaveBeenCalledTimes(1);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -48,6 +48,13 @@ function mockTaskBridge(): TaskBridge & {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Flush microtasks and any pending setTimeout(0)-ish waits used by the bridge */
|
|
52
|
+
async function flushAsync(iterations = 5): Promise<void> {
|
|
53
|
+
for (let i = 0; i < iterations; i++) {
|
|
54
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
describe("LifecycleBridge", () => {
|
|
52
59
|
let conn: ReturnType<typeof mockConnection>;
|
|
53
60
|
const scope = "swarm:test";
|
|
@@ -56,7 +63,7 @@ describe("LifecycleBridge", () => {
|
|
|
56
63
|
conn = mockConnection();
|
|
57
64
|
});
|
|
58
65
|
|
|
59
|
-
it("registers agent with MAP hub on spawn event", () => {
|
|
66
|
+
it("registers agent with MAP hub on spawn event", async () => {
|
|
60
67
|
const { callback } = createLifecycleBridge(
|
|
61
68
|
conn,
|
|
62
69
|
{} as AgentStore,
|
|
@@ -68,6 +75,8 @@ describe("LifecycleBridge", () => {
|
|
|
68
75
|
agent: mockAgent({ id: "agent-1", name: "worker-1", role: "worker" }),
|
|
69
76
|
});
|
|
70
77
|
|
|
78
|
+
await flushAsync();
|
|
79
|
+
|
|
71
80
|
expect(conn.callExtension).toHaveBeenCalledWith(
|
|
72
81
|
"map/agents/register",
|
|
73
82
|
expect.objectContaining({
|
|
@@ -77,7 +86,7 @@ describe("LifecycleBridge", () => {
|
|
|
77
86
|
);
|
|
78
87
|
});
|
|
79
88
|
|
|
80
|
-
it("includes per-agent ACP capabilities for coordinators", () => {
|
|
89
|
+
it("includes per-agent ACP capabilities for coordinators", async () => {
|
|
81
90
|
const { callback } = createLifecycleBridge(
|
|
82
91
|
conn,
|
|
83
92
|
{} as AgentStore,
|
|
@@ -89,6 +98,8 @@ describe("LifecycleBridge", () => {
|
|
|
89
98
|
agent: mockAgent({ id: "coord-1", name: "coordinator-1", role: "coordinator" }),
|
|
90
99
|
});
|
|
91
100
|
|
|
101
|
+
await flushAsync();
|
|
102
|
+
|
|
92
103
|
expect(conn.callExtension).toHaveBeenCalledWith(
|
|
93
104
|
"map/agents/register",
|
|
94
105
|
expect.objectContaining({
|
|
@@ -103,7 +114,7 @@ describe("LifecycleBridge", () => {
|
|
|
103
114
|
);
|
|
104
115
|
});
|
|
105
116
|
|
|
106
|
-
it("does not include ACP capabilities for workers", () => {
|
|
117
|
+
it("does not include ACP capabilities for workers", async () => {
|
|
107
118
|
const { callback } = createLifecycleBridge(
|
|
108
119
|
conn,
|
|
109
120
|
{} as AgentStore,
|
|
@@ -115,6 +126,8 @@ describe("LifecycleBridge", () => {
|
|
|
115
126
|
agent: mockAgent({ id: "worker-1", name: "worker-1", role: "worker" }),
|
|
116
127
|
});
|
|
117
128
|
|
|
129
|
+
await flushAsync();
|
|
130
|
+
|
|
118
131
|
const call = conn.callExtension.mock.calls[0];
|
|
119
132
|
const params = call[1] as Record<string, unknown>;
|
|
120
133
|
const caps = params.capabilities as Record<string, unknown>;
|
|
@@ -123,7 +136,63 @@ describe("LifecycleBridge", () => {
|
|
|
123
136
|
expect(caps.messaging).toEqual({ canReceive: true });
|
|
124
137
|
});
|
|
125
138
|
|
|
126
|
-
it("
|
|
139
|
+
it("includes peerMapId in metadata when getLocalMapId resolves", async () => {
|
|
140
|
+
const getLocalMapId = vi.fn((id: string) =>
|
|
141
|
+
id === "coord-1" ? "map-ulid-local" : undefined,
|
|
142
|
+
);
|
|
143
|
+
const { callback } = createLifecycleBridge(
|
|
144
|
+
conn,
|
|
145
|
+
{} as AgentStore,
|
|
146
|
+
scope,
|
|
147
|
+
undefined,
|
|
148
|
+
getLocalMapId,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
callback({
|
|
152
|
+
type: "spawned",
|
|
153
|
+
agent: mockAgent({ id: "coord-1", name: "coordinator-1", role: "coordinator" }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await flushAsync();
|
|
157
|
+
|
|
158
|
+
const call = conn.callExtension.mock.calls.find(
|
|
159
|
+
(c: any[]) => c[0] === "map/agents/register",
|
|
160
|
+
);
|
|
161
|
+
expect(call).toBeDefined();
|
|
162
|
+
const params = call![1] as Record<string, unknown>;
|
|
163
|
+
const metadata = params.metadata as Record<string, unknown>;
|
|
164
|
+
expect(metadata.peerMapId).toBe("map-ulid-local");
|
|
165
|
+
expect(metadata.peerAgentId).toBe("coord-1");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("registers without peerMapId when lookup returns undefined", async () => {
|
|
169
|
+
const getLocalMapId = vi.fn(() => undefined);
|
|
170
|
+
const { callback } = createLifecycleBridge(
|
|
171
|
+
conn,
|
|
172
|
+
{} as AgentStore,
|
|
173
|
+
scope,
|
|
174
|
+
undefined,
|
|
175
|
+
getLocalMapId,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
callback({
|
|
179
|
+
type: "spawned",
|
|
180
|
+
agent: mockAgent({ id: "coord-1", role: "coordinator" }),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Use a longer wait since the bridge will poll ~500ms for the local MAP ID
|
|
184
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
185
|
+
|
|
186
|
+
const call = conn.callExtension.mock.calls.find(
|
|
187
|
+
(c: any[]) => c[0] === "map/agents/register",
|
|
188
|
+
);
|
|
189
|
+
expect(call).toBeDefined();
|
|
190
|
+
const params = call![1] as Record<string, unknown>;
|
|
191
|
+
const metadata = params.metadata as Record<string, unknown>;
|
|
192
|
+
expect(metadata.peerMapId).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("unregisters agent from MAP hub on stop event", async () => {
|
|
127
196
|
const { callback } = createLifecycleBridge(
|
|
128
197
|
conn,
|
|
129
198
|
{} as AgentStore,
|
|
@@ -132,6 +201,7 @@ describe("LifecycleBridge", () => {
|
|
|
132
201
|
|
|
133
202
|
// First spawn, then stop
|
|
134
203
|
callback({ type: "spawned", agent: mockAgent({ id: "agent-1" }) });
|
|
204
|
+
await flushAsync();
|
|
135
205
|
callback({
|
|
136
206
|
type: "stopped",
|
|
137
207
|
agent: mockAgent({ id: "agent-1" }),
|
|
@@ -157,8 +227,8 @@ describe("LifecycleBridge", () => {
|
|
|
157
227
|
|
|
158
228
|
callback({ type: "spawned", agent: mockAgent({ id: "agent-1" }) });
|
|
159
229
|
|
|
160
|
-
// Wait for
|
|
161
|
-
await
|
|
230
|
+
// Wait for spawn registration + mapId capture
|
|
231
|
+
await flushAsync();
|
|
162
232
|
|
|
163
233
|
callback({
|
|
164
234
|
type: "stopped",
|
|
@@ -174,7 +244,7 @@ describe("LifecycleBridge", () => {
|
|
|
174
244
|
);
|
|
175
245
|
});
|
|
176
246
|
|
|
177
|
-
it("does nothing when disconnected", () => {
|
|
247
|
+
it("does nothing when disconnected", async () => {
|
|
178
248
|
const disconnected = {
|
|
179
249
|
...conn,
|
|
180
250
|
get isConnected() {
|
|
@@ -189,6 +259,8 @@ describe("LifecycleBridge", () => {
|
|
|
189
259
|
|
|
190
260
|
callback({ type: "spawned", agent: mockAgent() });
|
|
191
261
|
|
|
262
|
+
await flushAsync();
|
|
263
|
+
|
|
192
264
|
expect(conn.callExtension).not.toHaveBeenCalled();
|
|
193
265
|
});
|
|
194
266
|
|
|
@@ -246,16 +318,16 @@ describe("LifecycleBridge", () => {
|
|
|
246
318
|
callback({ type: "spawned", agent: mockAgent({ id: "a1" }) });
|
|
247
319
|
callback({ type: "spawned", agent: mockAgent({ id: "a2" }) });
|
|
248
320
|
|
|
321
|
+
await flushAsync();
|
|
249
322
|
await cleanup();
|
|
250
323
|
|
|
251
|
-
// 2 register calls + 2 unregister calls
|
|
252
324
|
const unregisterCalls = conn.callExtension.mock.calls.filter(
|
|
253
325
|
(c: any[]) => c[0] === "map/agents/unregister",
|
|
254
326
|
);
|
|
255
327
|
expect(unregisterCalls).toHaveLength(2);
|
|
256
328
|
});
|
|
257
329
|
|
|
258
|
-
it("silently handles MAP call failures", () => {
|
|
330
|
+
it("silently handles MAP call failures", async () => {
|
|
259
331
|
conn.callExtension.mockRejectedValue(new Error("network error"));
|
|
260
332
|
|
|
261
333
|
const { callback } = createLifecycleBridge(
|
|
@@ -268,9 +340,11 @@ describe("LifecycleBridge", () => {
|
|
|
268
340
|
expect(() => {
|
|
269
341
|
callback({ type: "spawned", agent: mockAgent() });
|
|
270
342
|
}).not.toThrow();
|
|
343
|
+
|
|
344
|
+
await flushAsync();
|
|
271
345
|
});
|
|
272
346
|
|
|
273
|
-
it("uses agent.id as fallback name when name is undefined", () => {
|
|
347
|
+
it("uses agent.id as fallback name when name is undefined", async () => {
|
|
274
348
|
const { callback } = createLifecycleBridge(
|
|
275
349
|
conn,
|
|
276
350
|
{} as AgentStore,
|
|
@@ -282,6 +356,8 @@ describe("LifecycleBridge", () => {
|
|
|
282
356
|
agent: mockAgent({ id: "agent-99", name: undefined }),
|
|
283
357
|
});
|
|
284
358
|
|
|
359
|
+
await flushAsync();
|
|
360
|
+
|
|
285
361
|
expect(conn.callExtension).toHaveBeenCalledWith(
|
|
286
362
|
"map/agents/register",
|
|
287
363
|
expect.objectContaining({ name: "agent-99" }),
|
package/src/map/acp-bridge.ts
CHANGED
|
@@ -39,7 +39,8 @@ interface ACPEnvelope {
|
|
|
39
39
|
/** Active ACP stream state */
|
|
40
40
|
interface ACPStreamState {
|
|
41
41
|
streamId: string;
|
|
42
|
-
|
|
42
|
+
/** Macro-agent's internal store id (the agent the stream targets). */
|
|
43
|
+
peerAgentId: string;
|
|
43
44
|
/** Push inbound ACP messages into the readable side */
|
|
44
45
|
push: (message: AnyMessage) => void;
|
|
45
46
|
/** Close the stream */
|
|
@@ -59,6 +60,13 @@ export interface ACPBridge {
|
|
|
59
60
|
*/
|
|
60
61
|
handleDelivery(agentId: string, message: any): boolean;
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Inspect which agent each open ACP stream is bound to. Used for
|
|
65
|
+
* observability and routing tests — the binding is otherwise per-stream
|
|
66
|
+
* in-memory state and not externally observable.
|
|
67
|
+
*/
|
|
68
|
+
getStreamBindings(): Array<{ streamId: string; peerAgentId: string }>;
|
|
69
|
+
|
|
62
70
|
/** Close all active streams */
|
|
63
71
|
close(): void;
|
|
64
72
|
}
|
|
@@ -212,14 +220,22 @@ export function createACPBridge(
|
|
|
212
220
|
// Create the AgentSideConnection.
|
|
213
221
|
// All outbound messages (responses + notifications) go through the
|
|
214
222
|
// writable side of the stream, which calls sendToClient above.
|
|
223
|
+
//
|
|
224
|
+
// Bind the MacroAgent to this stream's target agent so `session/new`
|
|
225
|
+
// creates a session for the agent the MAP stream was opened against,
|
|
226
|
+
// not whichever head manager happens to share the same cwd.
|
|
215
227
|
const conn = new AgentSideConnection(
|
|
216
|
-
(agentConn) =>
|
|
228
|
+
(agentConn) =>
|
|
229
|
+
createMacroAgent(agentConn, {
|
|
230
|
+
system,
|
|
231
|
+
initConfig: { targetAgentId: agentId },
|
|
232
|
+
}),
|
|
217
233
|
stream,
|
|
218
234
|
);
|
|
219
235
|
|
|
220
236
|
const state: ACPStreamState = {
|
|
221
237
|
streamId,
|
|
222
|
-
agentId,
|
|
238
|
+
peerAgentId: agentId,
|
|
223
239
|
push,
|
|
224
240
|
close,
|
|
225
241
|
connection: conn,
|
|
@@ -260,6 +276,13 @@ export function createACPBridge(
|
|
|
260
276
|
return true;
|
|
261
277
|
},
|
|
262
278
|
|
|
279
|
+
getStreamBindings(): Array<{ streamId: string; peerAgentId: string }> {
|
|
280
|
+
return Array.from(streams.values()).map((s) => ({
|
|
281
|
+
streamId: s.streamId,
|
|
282
|
+
peerAgentId: s.peerAgentId,
|
|
283
|
+
}));
|
|
284
|
+
},
|
|
285
|
+
|
|
263
286
|
close(): void {
|
|
264
287
|
for (const state of streams.values()) {
|
|
265
288
|
state.close();
|