macro-agent 0.1.7 → 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 +179 -38
- 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 -71
- 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 +34 -37
- 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 -8
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +76 -22
- 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 -4
- 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 +71 -11
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +293 -77
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +87 -36
- package/src/cli/index.ts +61 -0
- package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
- 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 +165 -22
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-bridge.ts +301 -0
- package/src/map/lifecycle-bridge.ts +77 -27
- package/src/map/server.ts +47 -6
- package/src/map/sidecar.ts +31 -3
- 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,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace V3 End-to-End Tests (Phase 4/5/7 integration gate)
|
|
3
|
+
*
|
|
4
|
+
* Exercises the full stream-first path:
|
|
5
|
+
* YAML → parseTeamWorkspaceConfig → YamlDrivenTopology → AgentManagerV2.spawn
|
|
6
|
+
* → executeWorkspaceDecision → WorkspaceManager V3 → real git-cascade + git
|
|
7
|
+
*
|
|
8
|
+
* These tests prevent the silent-failure class that mocked unit tests miss.
|
|
9
|
+
*
|
|
10
|
+
* REQUIRES: RUN_E2E_TESTS=true
|
|
11
|
+
*
|
|
12
|
+
* Run with:
|
|
13
|
+
* RUN_E2E_TESTS=true npx vitest run --config vitest.e2e.config.ts \
|
|
14
|
+
* src/__tests__/e2e/workspace-v3.e2e.test.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
describe,
|
|
19
|
+
it,
|
|
20
|
+
expect,
|
|
21
|
+
beforeEach,
|
|
22
|
+
afterEach,
|
|
23
|
+
vi,
|
|
24
|
+
} from 'vitest';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
import * as os from 'os';
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
import { execSync } from 'child_process';
|
|
29
|
+
import { bootV2, type MacroAgentSystemV2 } from '../../boot-v2.js';
|
|
30
|
+
import { GitCascadeAdapter } from '../../workspace/git-cascade-adapter.js';
|
|
31
|
+
import {
|
|
32
|
+
DefaultWorkspaceManager,
|
|
33
|
+
createWorkspaceManagerWithAdapter,
|
|
34
|
+
} from '../../workspace/workspace-manager.js';
|
|
35
|
+
import { YamlDrivenTopology } from '../../workspace/topology/yaml-driven.js';
|
|
36
|
+
import {
|
|
37
|
+
parseTeamWorkspaceConfig,
|
|
38
|
+
type TeamWorkspaceConfig,
|
|
39
|
+
} from '../../workspace/yaml-schema.js';
|
|
40
|
+
import {
|
|
41
|
+
registerBuiltinLandingStrategies,
|
|
42
|
+
MergeToParentStrategy,
|
|
43
|
+
} from '../../workspace/landing/index.js';
|
|
44
|
+
import {
|
|
45
|
+
DeferStrategy,
|
|
46
|
+
AbandonStrategy,
|
|
47
|
+
buildBuiltinRecoveryRegistry,
|
|
48
|
+
} from '../../workspace/recovery/index.js';
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────
|
|
51
|
+
// Harness
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const RUN_E2E = !!process.env.RUN_E2E_TESTS;
|
|
55
|
+
const describeFn = RUN_E2E ? describe : describe.skip;
|
|
56
|
+
|
|
57
|
+
// Mock acp-factory — no real Claude Code sessions
|
|
58
|
+
vi.mock('acp-factory', () => ({
|
|
59
|
+
AgentFactory: {
|
|
60
|
+
spawn: vi.fn().mockResolvedValue({
|
|
61
|
+
createSession: vi.fn().mockResolvedValue({
|
|
62
|
+
id: `session-${Date.now()}`,
|
|
63
|
+
prompt: vi.fn().mockReturnValue({
|
|
64
|
+
[Symbol.asyncIterator]: () => ({
|
|
65
|
+
next: () => Promise.resolve({ done: true, value: undefined }),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
forkWithFlush: vi.fn().mockResolvedValue({ id: `forked-${Date.now()}` }),
|
|
69
|
+
}),
|
|
70
|
+
loadSession: vi.fn().mockResolvedValue({ id: `loaded-${Date.now()}` }),
|
|
71
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
72
|
+
isRunning: vi.fn().mockReturnValue(true),
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
vi.mock('opentasks', () => ({
|
|
78
|
+
OpenTasksClient: vi.fn().mockImplementation(() => ({
|
|
79
|
+
connect: vi.fn().mockRejectedValue(new Error('No daemon')),
|
|
80
|
+
disconnect: vi.fn(),
|
|
81
|
+
query: vi.fn().mockResolvedValue({ items: [] }),
|
|
82
|
+
link: vi.fn().mockResolvedValue({ success: true }),
|
|
83
|
+
task: vi.fn().mockResolvedValue({ id: 't-1' }),
|
|
84
|
+
})),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
function createTestDir(): string {
|
|
88
|
+
const dir = path.join(
|
|
89
|
+
os.tmpdir(),
|
|
90
|
+
`ws-v3-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
91
|
+
);
|
|
92
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
93
|
+
return dir;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createGitRepo(baseDir: string): string {
|
|
97
|
+
const repoPath = path.join(baseDir, 'repo');
|
|
98
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
99
|
+
execSync('git init -b main', { cwd: repoPath, stdio: 'pipe' });
|
|
100
|
+
execSync('git config user.email "test@test.com"', { cwd: repoPath, stdio: 'pipe' });
|
|
101
|
+
execSync('git config user.name "Test User"', { cwd: repoPath, stdio: 'pipe' });
|
|
102
|
+
fs.writeFileSync(path.join(repoPath, 'README.md'), '# Test\n');
|
|
103
|
+
execSync('git add .', { cwd: repoPath, stdio: 'pipe' });
|
|
104
|
+
execSync('git commit -m "init"', { cwd: repoPath, stdio: 'pipe' });
|
|
105
|
+
return repoPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────
|
|
109
|
+
// Tests
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describeFn('Workspace V3 E2E', () => {
|
|
113
|
+
let system: MacroAgentSystemV2;
|
|
114
|
+
let testDir: string;
|
|
115
|
+
let repoPath: string;
|
|
116
|
+
let adapter: GitCascadeAdapter;
|
|
117
|
+
let workspaceManager: DefaultWorkspaceManager;
|
|
118
|
+
|
|
119
|
+
beforeEach(async () => {
|
|
120
|
+
testDir = createTestDir();
|
|
121
|
+
repoPath = createGitRepo(testDir);
|
|
122
|
+
const dbPath = path.join(testDir, 'git-cascade.db');
|
|
123
|
+
|
|
124
|
+
adapter = new GitCascadeAdapter({
|
|
125
|
+
enabled: true,
|
|
126
|
+
repoPath,
|
|
127
|
+
dbPath,
|
|
128
|
+
});
|
|
129
|
+
workspaceManager = createWorkspaceManagerWithAdapter(adapter, {
|
|
130
|
+
worktreeBaseDir: path.join(repoPath, '.worktrees'),
|
|
131
|
+
}) as DefaultWorkspaceManager;
|
|
132
|
+
|
|
133
|
+
// Register built-in landing strategies
|
|
134
|
+
registerBuiltinLandingStrategies(workspaceManager);
|
|
135
|
+
|
|
136
|
+
system = await bootV2({
|
|
137
|
+
cwd: repoPath,
|
|
138
|
+
baseDir: testDir,
|
|
139
|
+
inbox: { socketPath: path.join(testDir, 'inbox.sock') },
|
|
140
|
+
workspaceManager,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(async () => {
|
|
145
|
+
if (system) await system.shutdown();
|
|
146
|
+
if (workspaceManager) workspaceManager.close();
|
|
147
|
+
if (adapter) adapter.close();
|
|
148
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════
|
|
152
|
+
// TEST 1: Peer swarm — YAML → Topology → Spawn → V3 allocation
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════
|
|
154
|
+
|
|
155
|
+
describe('peer swarm (V3)', () => {
|
|
156
|
+
let topology: YamlDrivenTopology;
|
|
157
|
+
let config: TeamWorkspaceConfig;
|
|
158
|
+
|
|
159
|
+
beforeEach(async () => {
|
|
160
|
+
const parsed = parseTeamWorkspaceConfig({
|
|
161
|
+
roles: {
|
|
162
|
+
orchestrator: { workspace: 'none' },
|
|
163
|
+
peer: {
|
|
164
|
+
workspace: 'new_stream',
|
|
165
|
+
stream_lineage: 'fork_from_team_root',
|
|
166
|
+
landing: 'merge_to_parent_stream',
|
|
167
|
+
capabilities: ['workspace.commit', 'workspace.land'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
if (!parsed) throw new Error('config should parse');
|
|
172
|
+
config = parsed;
|
|
173
|
+
topology = new YamlDrivenTopology(config);
|
|
174
|
+
system.agentManager.setTopologyPolicy(topology);
|
|
175
|
+
|
|
176
|
+
// Simulate team start (TeamManager would do this; we call manually)
|
|
177
|
+
await topology.onTeamStart({
|
|
178
|
+
teamName: 'peer-swarm-test',
|
|
179
|
+
teamInstanceId: 'ps-1',
|
|
180
|
+
workspaceConfig: config,
|
|
181
|
+
workspaceManager,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('creates a team root stream at onTeamStart', () => {
|
|
186
|
+
const streams = workspaceManager.listStreams();
|
|
187
|
+
const teamRoot = streams.find((s) => s.agentId === 'team:peer-swarm-test');
|
|
188
|
+
expect(teamRoot).toBeDefined();
|
|
189
|
+
expect(teamRoot?.name).toBe('peer-swarm-test');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('spawns orchestrator with no workspace (workspace: none)', async () => {
|
|
193
|
+
const orch = await system.agentManager.spawn({
|
|
194
|
+
role: 'orchestrator',
|
|
195
|
+
task: 'coordinate',
|
|
196
|
+
});
|
|
197
|
+
const record = system.agentStore.getAgent(orch.id);
|
|
198
|
+
// workspace: none → no workspace_path assigned
|
|
199
|
+
expect(record?.workspace_path).toBeFalsy();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('spawns peer with a new stream forked off team root', async () => {
|
|
203
|
+
const peer = await system.agentManager.spawn({
|
|
204
|
+
role: 'peer',
|
|
205
|
+
task: 'investigate',
|
|
206
|
+
});
|
|
207
|
+
const record = system.agentStore.getAgent(peer.id);
|
|
208
|
+
|
|
209
|
+
expect(record?.workspace_path).toBeDefined();
|
|
210
|
+
expect(fs.existsSync(record!.workspace_path!)).toBe(true);
|
|
211
|
+
|
|
212
|
+
const streams = workspaceManager.listStreams();
|
|
213
|
+
const teamRoot = streams.find((s) => s.agentId === 'team:peer-swarm-test');
|
|
214
|
+
const peerStream = streams.find((s) => s.agentId === peer.id);
|
|
215
|
+
|
|
216
|
+
expect(peerStream).toBeDefined();
|
|
217
|
+
expect(peerStream?.parentStream).toBe(teamRoot?.id);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('emits stream:created and worktree:allocated events on peer spawn', async () => {
|
|
221
|
+
const events: string[] = [];
|
|
222
|
+
workspaceManager.onEvent((e) => events.push(e.type));
|
|
223
|
+
|
|
224
|
+
await system.agentManager.spawn({ role: 'peer', task: 'investigate' });
|
|
225
|
+
|
|
226
|
+
expect(events).toContain('stream:forked');
|
|
227
|
+
expect(events).toContain('worktree:allocated');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════
|
|
232
|
+
// TEST 2: Landing — real merge-to-parent through a real merge
|
|
233
|
+
// ═══════════════════════════════════════════════════════════════
|
|
234
|
+
|
|
235
|
+
describe('merge-to-parent landing (V3)', () => {
|
|
236
|
+
it('merges a child stream into its parent via the strategy', async () => {
|
|
237
|
+
// Team root owned by a pseudo-principal
|
|
238
|
+
const parentStreamId = workspaceManager.createStreamV3({
|
|
239
|
+
name: 'parent',
|
|
240
|
+
ownerId: 'team:landing-test',
|
|
241
|
+
forkFrom: 'main',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Agent owns a child forked off parent
|
|
245
|
+
const childStreamId = workspaceManager.forkStream({
|
|
246
|
+
parentStreamId,
|
|
247
|
+
name: 'child',
|
|
248
|
+
ownerId: 'agent-auth',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Allocate worktree for the agent on the child branch
|
|
252
|
+
const worktree = workspaceManager.allocateWorktree({
|
|
253
|
+
agentId: 'agent-auth',
|
|
254
|
+
streamId: childStreamId,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Make a real commit via commitChanges (Change-Id tracked)
|
|
258
|
+
fs.writeFileSync(path.join(worktree.path, 'auth.ts'), 'export const X = 1;');
|
|
259
|
+
const { commit, changeId } = workspaceManager.commitChanges({
|
|
260
|
+
agentId: 'agent-auth',
|
|
261
|
+
streamId: childStreamId,
|
|
262
|
+
worktree: worktree.path,
|
|
263
|
+
message: 'feat: add auth module',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(commit).toMatch(/^[0-9a-f]+$/);
|
|
267
|
+
expect(changeId).toMatch(/^c-/);
|
|
268
|
+
|
|
269
|
+
// Invoke the landing strategy directly
|
|
270
|
+
const strategy = new MergeToParentStrategy();
|
|
271
|
+
const result = await strategy.land({
|
|
272
|
+
agentId: 'agent-auth',
|
|
273
|
+
streamId: childStreamId,
|
|
274
|
+
sourceWorktree: worktree.path,
|
|
275
|
+
workspaceManager,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(result.success).toBe(true);
|
|
279
|
+
|
|
280
|
+
// Verify parent stream advanced
|
|
281
|
+
const parent = workspaceManager.listStreams().find((s) => s.id === parentStreamId);
|
|
282
|
+
expect(parent?.status).toBe('active');
|
|
283
|
+
|
|
284
|
+
// Verify the Change-Id is tracked and findable
|
|
285
|
+
const change = workspaceManager.getChange(changeId);
|
|
286
|
+
expect(change).not.toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('fails gracefully when source has no parent', async () => {
|
|
290
|
+
// Orphan stream (no parent)
|
|
291
|
+
const streamId = workspaceManager.createStreamV3({
|
|
292
|
+
name: 'orphan',
|
|
293
|
+
ownerId: 'agent-x',
|
|
294
|
+
forkFrom: 'main',
|
|
295
|
+
});
|
|
296
|
+
const worktree = workspaceManager.allocateWorktree({
|
|
297
|
+
agentId: 'agent-x',
|
|
298
|
+
streamId,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const strategy = new MergeToParentStrategy();
|
|
302
|
+
const result = await strategy.land({
|
|
303
|
+
agentId: 'agent-x',
|
|
304
|
+
streamId,
|
|
305
|
+
sourceWorktree: worktree.path,
|
|
306
|
+
workspaceManager,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(result.success).toBe(false);
|
|
310
|
+
expect(result.error).toMatch(/no target stream/);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════
|
|
315
|
+
// TEST 3: Conflict recovery — real conflict → real abandon
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════
|
|
317
|
+
|
|
318
|
+
describe('conflict recovery dispatch (V3)', () => {
|
|
319
|
+
it('routes through buildBuiltinRecoveryRegistry to the abandon strategy', async () => {
|
|
320
|
+
// Create a stream so abandon has something to act on
|
|
321
|
+
const streamId = workspaceManager.createStreamV3({
|
|
322
|
+
name: 'doomed',
|
|
323
|
+
ownerId: 'agent-1',
|
|
324
|
+
forkFrom: 'main',
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Simulate a conflict record created by git-cascade
|
|
328
|
+
const conflictId = adapter.createConflict({
|
|
329
|
+
streamId,
|
|
330
|
+
conflictingCommit: '0'.repeat(40),
|
|
331
|
+
targetCommit: '0'.repeat(40),
|
|
332
|
+
conflictedFiles: ['conflict.ts'],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const registry = buildBuiltinRecoveryRegistry();
|
|
336
|
+
const abandon = registry.get('abandon');
|
|
337
|
+
expect(abandon).toBeDefined();
|
|
338
|
+
|
|
339
|
+
const resolution = await abandon!.recover({
|
|
340
|
+
conflictId,
|
|
341
|
+
streamId,
|
|
342
|
+
paths: ['conflict.ts'],
|
|
343
|
+
operation: 'merge',
|
|
344
|
+
recoveryDepth: 0,
|
|
345
|
+
workspaceManager,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(resolution.kind).toBe('abandoned');
|
|
349
|
+
// Verify the stream is actually abandoned in git-cascade
|
|
350
|
+
const stream = workspaceManager.listStreams().find((s) => s.id === streamId);
|
|
351
|
+
expect(stream?.status).toBe('abandoned');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('defer leaves the conflict record in place', async () => {
|
|
355
|
+
const streamId = workspaceManager.createStreamV3({
|
|
356
|
+
name: 'deferred',
|
|
357
|
+
ownerId: 'agent-1',
|
|
358
|
+
forkFrom: 'main',
|
|
359
|
+
});
|
|
360
|
+
const conflictId = adapter.createConflict({
|
|
361
|
+
streamId,
|
|
362
|
+
conflictingCommit: '0'.repeat(40),
|
|
363
|
+
targetCommit: '0'.repeat(40),
|
|
364
|
+
conflictedFiles: ['x.ts'],
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const defer = new DeferStrategy();
|
|
368
|
+
const resolution = await defer.recover({
|
|
369
|
+
conflictId,
|
|
370
|
+
streamId,
|
|
371
|
+
paths: ['x.ts'],
|
|
372
|
+
operation: 'merge',
|
|
373
|
+
recoveryDepth: 0,
|
|
374
|
+
workspaceManager,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(resolution.kind).toBe('deferred');
|
|
378
|
+
// Stream is untouched (still active; conflict record still exists)
|
|
379
|
+
const stream = workspaceManager.listStreams().find((s) => s.id === streamId);
|
|
380
|
+
expect(stream?.status).toBe('active');
|
|
381
|
+
const conflict = adapter.getConflict(conflictId);
|
|
382
|
+
expect(conflict).not.toBeNull();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ═══════════════════════════════════════════════════════════════
|
|
387
|
+
// TEST 4: Regression — legacy path still works unchanged
|
|
388
|
+
// ═══════════════════════════════════════════════════════════════
|
|
389
|
+
|
|
390
|
+
describe('regression: legacy path (no TopologyPolicy set)', () => {
|
|
391
|
+
it('still allocates workspace via role-name dispatch', async () => {
|
|
392
|
+
// Do NOT set a topology policy; ensure legacy path is active
|
|
393
|
+
const streamId = workspaceManager.createIntegrationStream('coord-legacy', {
|
|
394
|
+
name: 'legacy-feature',
|
|
395
|
+
});
|
|
396
|
+
const taskId = workspaceManager.createTask(streamId, {
|
|
397
|
+
title: 'legacy task',
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const worker = await system.agentManager.spawn({
|
|
401
|
+
role: 'worker',
|
|
402
|
+
task: 'do work',
|
|
403
|
+
streamId,
|
|
404
|
+
gitCascadeTaskId: taskId,
|
|
405
|
+
capabilities: ['workspace.worktree'],
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const record = system.agentStore.getAgent(worker.id);
|
|
409
|
+
expect(record?.workspace_path).toBeDefined();
|
|
410
|
+
expect(fs.existsSync(record!.workspace_path!)).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Claude Code JSONL → ACP SessionUpdate converter.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the replay fallback correctly parses real-world Claude Code
|
|
5
|
+
* transcript entries and emits the right sessionUpdate events in order.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
import { replayClaudeCodeTranscript } from "../claude-code-replay.js";
|
|
14
|
+
|
|
15
|
+
function jsonl(obj: unknown): string {
|
|
16
|
+
return JSON.stringify(obj) + "\n";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function collectUpdates(sessionId: string): Promise<any[]> {
|
|
20
|
+
const updates: any[] = [];
|
|
21
|
+
for await (const u of replayClaudeCodeTranscript(sessionId)) {
|
|
22
|
+
updates.push(u);
|
|
23
|
+
}
|
|
24
|
+
return updates;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("replayClaudeCodeTranscript", () => {
|
|
28
|
+
const testProjectDir = `-test-replay-${randomBytes(4).toString("hex")}`;
|
|
29
|
+
const claudeProjectsRoot = path.join(os.homedir(), ".claude", "projects");
|
|
30
|
+
const fixtureDir = path.join(claudeProjectsRoot, testProjectDir);
|
|
31
|
+
const nonExistentSessionId = "00000000-0000-0000-0000-000000000000";
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
await fs.mkdir(fixtureDir, { recursive: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterAll(async () => {
|
|
38
|
+
await fs.rm(fixtureDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns no updates when transcript file doesn't exist", async () => {
|
|
42
|
+
const updates = await collectUpdates(nonExistentSessionId);
|
|
43
|
+
expect(updates).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("converts a simple user/assistant exchange", async () => {
|
|
47
|
+
const sessionId = `sess-simple-${randomBytes(4).toString("hex")}`;
|
|
48
|
+
const transcript =
|
|
49
|
+
jsonl({ type: "queue-operation", operation: "enqueue" }) + // skip
|
|
50
|
+
jsonl({
|
|
51
|
+
type: "user",
|
|
52
|
+
isMeta: false,
|
|
53
|
+
message: { role: "user", content: [{ type: "text", text: "hello" }] },
|
|
54
|
+
}) +
|
|
55
|
+
jsonl({
|
|
56
|
+
type: "assistant",
|
|
57
|
+
message: {
|
|
58
|
+
role: "assistant",
|
|
59
|
+
content: [{ type: "text", text: "hi there" }],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
|
|
63
|
+
|
|
64
|
+
const updates = await collectUpdates(sessionId);
|
|
65
|
+
expect(updates).toHaveLength(2);
|
|
66
|
+
expect(updates[0]).toEqual({
|
|
67
|
+
sessionUpdate: "user_message_chunk",
|
|
68
|
+
content: { type: "text", text: "hello" },
|
|
69
|
+
});
|
|
70
|
+
expect(updates[1]).toEqual({
|
|
71
|
+
sessionUpdate: "agent_message_chunk",
|
|
72
|
+
content: { type: "text", text: "hi there" },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skips meta messages and internal commands", async () => {
|
|
77
|
+
const sessionId = `sess-meta-${randomBytes(4).toString("hex")}`;
|
|
78
|
+
const transcript =
|
|
79
|
+
jsonl({
|
|
80
|
+
type: "user",
|
|
81
|
+
isMeta: true, // should skip
|
|
82
|
+
message: { role: "user", content: "system context" },
|
|
83
|
+
}) +
|
|
84
|
+
jsonl({
|
|
85
|
+
type: "user",
|
|
86
|
+
message: { role: "user", content: "<command-name>/model</command-name>" },
|
|
87
|
+
}) + // string starting with <command- → skip
|
|
88
|
+
jsonl({
|
|
89
|
+
type: "user",
|
|
90
|
+
message: { role: "user", content: "<local-command-stdout>ok</local-command-stdout>" },
|
|
91
|
+
}) + // skip
|
|
92
|
+
jsonl({
|
|
93
|
+
type: "user",
|
|
94
|
+
message: { role: "user", content: [{ type: "text", text: "real message" }] },
|
|
95
|
+
});
|
|
96
|
+
await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
|
|
97
|
+
|
|
98
|
+
const updates = await collectUpdates(sessionId);
|
|
99
|
+
expect(updates).toHaveLength(1);
|
|
100
|
+
expect(updates[0]).toMatchObject({
|
|
101
|
+
sessionUpdate: "user_message_chunk",
|
|
102
|
+
content: { type: "text", text: "real message" },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("converts tool_use and tool_result blocks", async () => {
|
|
107
|
+
const sessionId = `sess-tools-${randomBytes(4).toString("hex")}`;
|
|
108
|
+
const transcript =
|
|
109
|
+
jsonl({
|
|
110
|
+
type: "assistant",
|
|
111
|
+
message: {
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: [
|
|
114
|
+
{ type: "text", text: "Let me check." },
|
|
115
|
+
{
|
|
116
|
+
type: "tool_use",
|
|
117
|
+
id: "tc_1",
|
|
118
|
+
name: "Read",
|
|
119
|
+
input: { path: "/tmp/foo" },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
}) +
|
|
124
|
+
jsonl({
|
|
125
|
+
type: "user",
|
|
126
|
+
message: {
|
|
127
|
+
role: "user",
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: "tool_result",
|
|
131
|
+
tool_use_id: "tc_1",
|
|
132
|
+
content: "file contents",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
|
|
138
|
+
|
|
139
|
+
const updates = await collectUpdates(sessionId);
|
|
140
|
+
expect(updates).toHaveLength(3);
|
|
141
|
+
expect(updates[0]).toMatchObject({
|
|
142
|
+
sessionUpdate: "agent_message_chunk",
|
|
143
|
+
content: { type: "text", text: "Let me check." },
|
|
144
|
+
});
|
|
145
|
+
expect(updates[1]).toMatchObject({
|
|
146
|
+
sessionUpdate: "tool_call",
|
|
147
|
+
toolCallId: "tc_1",
|
|
148
|
+
title: "Read",
|
|
149
|
+
rawInput: { path: "/tmp/foo" },
|
|
150
|
+
});
|
|
151
|
+
expect(updates[2]).toMatchObject({
|
|
152
|
+
sessionUpdate: "tool_call_update",
|
|
153
|
+
toolCallId: "tc_1",
|
|
154
|
+
output: "file contents",
|
|
155
|
+
status: "completed",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("handles assistant thinking blocks", async () => {
|
|
160
|
+
const sessionId = `sess-think-${randomBytes(4).toString("hex")}`;
|
|
161
|
+
const transcript = jsonl({
|
|
162
|
+
type: "assistant",
|
|
163
|
+
message: {
|
|
164
|
+
role: "assistant",
|
|
165
|
+
content: [
|
|
166
|
+
{ type: "thinking", thinking: "Let me reason about this..." },
|
|
167
|
+
{ type: "text", text: "Here's my answer." },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
|
|
172
|
+
|
|
173
|
+
const updates = await collectUpdates(sessionId);
|
|
174
|
+
expect(updates).toHaveLength(2);
|
|
175
|
+
expect(updates[0]).toMatchObject({
|
|
176
|
+
sessionUpdate: "agent_thought_chunk",
|
|
177
|
+
content: { type: "text", text: "Let me reason about this..." },
|
|
178
|
+
});
|
|
179
|
+
expect(updates[1]).toMatchObject({
|
|
180
|
+
sessionUpdate: "agent_message_chunk",
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("ignores malformed JSONL lines and continues", async () => {
|
|
185
|
+
const sessionId = `sess-bad-${randomBytes(4).toString("hex")}`;
|
|
186
|
+
const transcript =
|
|
187
|
+
"not-json\n" +
|
|
188
|
+
"{\n" + // incomplete
|
|
189
|
+
jsonl({
|
|
190
|
+
type: "user",
|
|
191
|
+
message: { role: "user", content: [{ type: "text", text: "after garbage" }] },
|
|
192
|
+
}) +
|
|
193
|
+
"\n"; // blank line
|
|
194
|
+
await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
|
|
195
|
+
|
|
196
|
+
const updates = await collectUpdates(sessionId);
|
|
197
|
+
expect(updates).toHaveLength(1);
|
|
198
|
+
expect(updates[0]).toMatchObject({
|
|
199
|
+
content: { type: "text", text: "after garbage" },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("produces events in chronological (file) order", async () => {
|
|
204
|
+
const sessionId = `sess-order-${randomBytes(4).toString("hex")}`;
|
|
205
|
+
const transcript = Array.from({ length: 5 }, (_, i) =>
|
|
206
|
+
jsonl({
|
|
207
|
+
type: i % 2 === 0 ? "user" : "assistant",
|
|
208
|
+
message: {
|
|
209
|
+
role: i % 2 === 0 ? "user" : "assistant",
|
|
210
|
+
content: [{ type: "text", text: `msg-${i}` }],
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
).join("");
|
|
214
|
+
await fs.writeFile(path.join(fixtureDir, `${sessionId}.jsonl`), transcript);
|
|
215
|
+
|
|
216
|
+
const updates = await collectUpdates(sessionId);
|
|
217
|
+
expect(updates.map((u) => u.content?.text)).toEqual([
|
|
218
|
+
"msg-0",
|
|
219
|
+
"msg-1",
|
|
220
|
+
"msg-2",
|
|
221
|
+
"msg-3",
|
|
222
|
+
"msg-4",
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -141,7 +141,7 @@ describe("createMacroAgent", () => {
|
|
|
141
141
|
// ── newSession ──────────────────────────────────────────────
|
|
142
142
|
|
|
143
143
|
describe("newSession", () => {
|
|
144
|
-
it("should create head manager and return session ID", async () => {
|
|
144
|
+
it("should create head manager and return session ID (cwd fallback)", async () => {
|
|
145
145
|
const agent = createAgent();
|
|
146
146
|
const result = await agent.newSession({
|
|
147
147
|
cwd: "/tmp/test",
|
|
@@ -153,6 +153,44 @@ describe("createMacroAgent", () => {
|
|
|
153
153
|
cwd: "/tmp/test",
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
it("binds the session to initConfig.targetAgentId when provided (skips cwd lookup)", async () => {
|
|
158
|
+
// Stub getActiveAgentSession on the mock system
|
|
159
|
+
system.agentManager.getActiveAgentSession = vi.fn().mockReturnValue({
|
|
160
|
+
id: "worker-7",
|
|
161
|
+
session_id: "worker-7-session",
|
|
162
|
+
agent: { id: "worker-7", role: "worker", state: "running" },
|
|
163
|
+
session: {},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Bind this MacroAgent to a specific (non-coordinator) agent
|
|
167
|
+
const agent = createMacroAgent(connection, {
|
|
168
|
+
system,
|
|
169
|
+
initConfig: { targetAgentId: "worker-7" },
|
|
170
|
+
});
|
|
171
|
+
await agent.newSession({ cwd: "/tmp/whatever", mcpServers: [] });
|
|
172
|
+
|
|
173
|
+
expect(system.agentManager.getActiveAgentSession).toHaveBeenCalledWith("worker-7");
|
|
174
|
+
// cwd-based lookup must NOT run when a target is bound
|
|
175
|
+
expect(system.agentManager.getOrCreateHeadManager).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("throws AGENT_NOT_FOUND when targetAgentId has no live session", async () => {
|
|
179
|
+
system.agentManager.getActiveAgentSession = vi.fn().mockReturnValue(null);
|
|
180
|
+
|
|
181
|
+
const agent = createMacroAgent(connection, {
|
|
182
|
+
system,
|
|
183
|
+
initConfig: { targetAgentId: "ghost-agent" },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
agent.newSession({ cwd: "/tmp/test", mcpServers: [] }),
|
|
188
|
+
).rejects.toMatchObject({
|
|
189
|
+
name: "ACPError",
|
|
190
|
+
code: "AGENT_NOT_FOUND",
|
|
191
|
+
});
|
|
192
|
+
expect(system.agentManager.getOrCreateHeadManager).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
156
194
|
});
|
|
157
195
|
|
|
158
196
|
// ── prompt ──────────────────────────────────────────────────
|