macro-agent 0.1.8 → 0.1.11
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 +263 -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 +192 -7
- 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/adapters/tasks-adapter.d.ts.map +1 -1
- package/dist/adapters/tasks-adapter.js +3 -0
- package/dist/adapters/tasks-adapter.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.d.ts.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 +308 -54
- 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/agent-store.d.ts +10 -0
- package/dist/agent/agent-store.d.ts.map +1 -1
- package/dist/agent/agent-store.js +22 -0
- package/dist/agent/agent-store.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 +129 -1
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +359 -8
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/acp.js +4 -0
- package/dist/cli/acp.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/cascade.d.ts +25 -2
- package/dist/lifecycle/cascade.d.ts.map +1 -1
- package/dist/lifecycle/cascade.js +70 -2
- package/dist/lifecycle/cascade.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-action-handler.d.ts +24 -0
- package/dist/map/cascade-action-handler.d.ts.map +1 -0
- package/dist/map/cascade-action-handler.js +170 -0
- package/dist/map/cascade-action-handler.js.map +1 -0
- 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 +294 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/coordination-handler.d.ts.map +1 -1
- package/dist/map/coordination-handler.js +12 -1
- package/dist/map/coordination-handler.js.map +1 -1
- 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 +219 -7
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +49 -2
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +22 -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 +934 -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 +186 -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 +118 -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 +117 -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 +162 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +101 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +416 -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/design/task-dispatcher.md +880 -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 +6 -5
- package/src/__tests__/boot-v2.test.ts +435 -0
- package/src/__tests__/e2e/acp-over-map.e2e.test.ts +92 -0
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
- package/src/__tests__/e2e/bootstrap.e2e.test.ts +319 -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/dispatch-coordination.e2e.test.ts +495 -0
- package/src/__tests__/e2e/dispatch-live.e2e.test.ts +564 -0
- package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +496 -0
- package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +456 -0
- package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +386 -0
- package/src/__tests__/e2e/dispatch.e2e.test.ts +376 -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 +203 -10
- package/src/acp/types.ts +10 -0
- package/src/adapters/__tests__/tasks-adapter.test.ts +1 -0
- package/src/adapters/tasks-adapter.ts +3 -0
- package/src/adapters/types.ts +1 -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__/agent-store.test.ts +52 -0
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +372 -59
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/agent-store.ts +24 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +589 -35
- package/src/cli/acp.ts +4 -0
- 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/__tests__/cascade-consolidation.test.ts +240 -0
- package/src/lifecycle/cascade.ts +77 -2
- 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__/emit-event.test.ts +71 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-action-handler.ts +205 -0
- package/src/map/cascade-bridge.ts +339 -0
- package/src/map/coordination-handler.ts +13 -1
- package/src/map/lifecycle-bridge.ts +52 -17
- package/src/map/server.ts +225 -7
- package/src/map/sidecar.ts +48 -1
- package/src/map/types.ts +23 -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__/land-dispatch.test.ts +214 -0
- 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 +1213 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +184 -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 +229 -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 +152 -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 +162 -0
- package/src/workspace/types.ts +211 -20
- package/src/workspace/workspace-manager.ts +533 -19
- package/src/workspace/yaml-schema.ts +216 -0
- package/dist/workspace/dataplane-adapter.d.ts +0 -260
- package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
- package/dist/workspace/dataplane-adapter.js +0 -416
- package/dist/workspace/dataplane-adapter.js.map +0 -1
- package/src/workspace/dataplane-adapter.ts +0 -546
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Dispatch Live Agent E2E Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the full dispatch lifecycle with REAL Claude Code agents:
|
|
5
|
+
* - Dispatch strategy spawns real agents for tasks
|
|
6
|
+
* - Agents complete work and call done()
|
|
7
|
+
* - Lifecycle listener detects completion and updates tracker
|
|
8
|
+
* - Reconciliation handles external state changes
|
|
9
|
+
*
|
|
10
|
+
* These tests hit the Claude API and require authenticated Claude Code.
|
|
11
|
+
*
|
|
12
|
+
* REQUIRES: RUN_FULL_AGENT_TESTS=true
|
|
13
|
+
*
|
|
14
|
+
* Run with:
|
|
15
|
+
* RUN_FULL_AGENT_TESTS=true npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/dispatch-live.e2e.test.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
describe,
|
|
20
|
+
it,
|
|
21
|
+
expect,
|
|
22
|
+
beforeEach,
|
|
23
|
+
afterEach,
|
|
24
|
+
vi,
|
|
25
|
+
} from "vitest";
|
|
26
|
+
import * as path from "path";
|
|
27
|
+
import * as os from "os";
|
|
28
|
+
import * as fs from "fs";
|
|
29
|
+
import { execSync } from "child_process";
|
|
30
|
+
import { bootV2, type MacroAgentSystemV2 } from "../../boot-v2.js";
|
|
31
|
+
import {
|
|
32
|
+
createTaskDispatcher,
|
|
33
|
+
type TaskDispatcher,
|
|
34
|
+
type DispatchAgentRuntime,
|
|
35
|
+
type DispatchTaskSource,
|
|
36
|
+
} from "swarm-dispatch";
|
|
37
|
+
import type { TaskRecord } from "../../adapters/types.js";
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────
|
|
40
|
+
// Configuration
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
|
|
44
|
+
const describeFn = RUN_FULL_AGENT ? describe : describe.skip;
|
|
45
|
+
|
|
46
|
+
const TIMEOUT = {
|
|
47
|
+
SPAWN: 60_000,
|
|
48
|
+
DISPATCH: 120_000,
|
|
49
|
+
MULTI: 180_000,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────
|
|
53
|
+
// Helpers
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function createTestRepo(prefix: string): { path: string; cleanup: () => void } {
|
|
57
|
+
const tmpDir = fs.mkdtempSync(
|
|
58
|
+
path.join(os.tmpdir(), `dispatch-live-${prefix}-`)
|
|
59
|
+
);
|
|
60
|
+
const repoPath = path.join(tmpDir, "test-repo");
|
|
61
|
+
fs.mkdirSync(repoPath);
|
|
62
|
+
execSync("git init", { cwd: repoPath, stdio: "pipe" });
|
|
63
|
+
execSync('git config user.email "test@test.com"', {
|
|
64
|
+
cwd: repoPath,
|
|
65
|
+
stdio: "pipe",
|
|
66
|
+
});
|
|
67
|
+
execSync('git config user.name "Test User"', {
|
|
68
|
+
cwd: repoPath,
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
});
|
|
71
|
+
fs.writeFileSync(path.join(repoPath, "README.md"), "# Test Repo\n");
|
|
72
|
+
execSync("git add -A", { cwd: repoPath, stdio: "pipe" });
|
|
73
|
+
execSync('git commit -m "Initial commit"', { cwd: repoPath, stdio: "pipe" });
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
path: repoPath,
|
|
77
|
+
cleanup: () => fs.rmSync(tmpDir, { recursive: true, force: true }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function log(msg: string): void {
|
|
82
|
+
console.log(`[DISPATCH-LIVE] ${msg}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sleep(ms: number): Promise<void> {
|
|
86
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Wait for a condition to become true, polling at intervals.
|
|
91
|
+
*/
|
|
92
|
+
async function waitFor(
|
|
93
|
+
condition: () => boolean,
|
|
94
|
+
timeoutMs: number = 30_000,
|
|
95
|
+
pollMs: number = 500
|
|
96
|
+
): Promise<boolean> {
|
|
97
|
+
const deadline = Date.now() + timeoutMs;
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
if (condition()) return true;
|
|
100
|
+
await sleep(pollMs);
|
|
101
|
+
}
|
|
102
|
+
return condition();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────
|
|
106
|
+
// Tests
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
describeFn("Task Dispatch Live Agent E2E", () => {
|
|
110
|
+
let system: MacroAgentSystemV2;
|
|
111
|
+
let testRepo: { path: string; cleanup: () => void };
|
|
112
|
+
let baseDir: string;
|
|
113
|
+
let dispatcher: TaskDispatcher;
|
|
114
|
+
|
|
115
|
+
function createSourceAdapter(tasksAdapter: typeof system.tasksAdapter): DispatchTaskSource {
|
|
116
|
+
return {
|
|
117
|
+
queryReady: (opts) => tasksAdapter.queryReady(opts),
|
|
118
|
+
claim: async (taskId, claimantId) => {
|
|
119
|
+
try {
|
|
120
|
+
await tasksAdapter.assignTask(taskId, claimantId);
|
|
121
|
+
return { success: true as const };
|
|
122
|
+
} catch { return { success: false as const }; }
|
|
123
|
+
},
|
|
124
|
+
release: async (taskId) => tasksAdapter.unclaimTask(taskId),
|
|
125
|
+
transition: async (taskId, action) => tasksAdapter.transitionTask(taskId, action),
|
|
126
|
+
getTask: async (taskId) => tasksAdapter.getTask(taskId),
|
|
127
|
+
listInProgress: async () => tasksAdapter.listTasks({ status: "in_progress" }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createRuntimeAdapter(agentManager: typeof system.agentManager): DispatchAgentRuntime {
|
|
132
|
+
return {
|
|
133
|
+
spawn: async (opts) => {
|
|
134
|
+
const spawned = await agentManager.spawn({
|
|
135
|
+
task: opts.prompt, task_id: opts.taskId, role: opts.role, parent: null,
|
|
136
|
+
});
|
|
137
|
+
return { id: spawned.id };
|
|
138
|
+
},
|
|
139
|
+
terminate: async (agentId) => agentManager.terminate(agentId, "cancelled"),
|
|
140
|
+
onStopped: (cb) => agentManager.onLifecycleEvent((event) => {
|
|
141
|
+
if (event.type === "stopped") cb(event.agent.id, event.reason);
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
beforeEach(async () => {
|
|
147
|
+
testRepo = createTestRepo("dispatch");
|
|
148
|
+
baseDir = path.join(testRepo.path, ".macro-agent");
|
|
149
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
system = await bootV2({
|
|
152
|
+
cwd: testRepo.path,
|
|
153
|
+
baseDir,
|
|
154
|
+
defaultPermissionMode: "auto-approve",
|
|
155
|
+
inbox: { socketPath: path.join(baseDir, "inbox.sock") },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
dispatcher = createTaskDispatcher(
|
|
159
|
+
createSourceAdapter(system.tasksAdapter),
|
|
160
|
+
createRuntimeAdapter(system.agentManager),
|
|
161
|
+
{
|
|
162
|
+
claimantId: `test:${process.pid}:dispatch-live`,
|
|
163
|
+
pollIntervalMs: 600_000,
|
|
164
|
+
defaultRole: "worker",
|
|
165
|
+
concurrency: { global: 3 },
|
|
166
|
+
retry: { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 60_000 },
|
|
167
|
+
reconcile: { enabled: true, intervalMs: 600_000 },
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
await dispatcher.start();
|
|
171
|
+
|
|
172
|
+
log("System booted with swarm-dispatch");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
afterEach(async () => {
|
|
176
|
+
if (dispatcher) {
|
|
177
|
+
await dispatcher.stop();
|
|
178
|
+
}
|
|
179
|
+
if (system) {
|
|
180
|
+
try {
|
|
181
|
+
const running = system.agentManager.list({ state: "running" });
|
|
182
|
+
for (const agent of running) {
|
|
183
|
+
try {
|
|
184
|
+
await system.agentManager.terminate(agent.id, "cancelled");
|
|
185
|
+
} catch {
|
|
186
|
+
// Best effort
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await system.shutdown();
|
|
190
|
+
} catch {
|
|
191
|
+
// Best effort
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
testRepo?.cleanup();
|
|
195
|
+
log("Cleanup complete");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── Dispatch spawns a real agent that completes ─────────────
|
|
199
|
+
|
|
200
|
+
it(
|
|
201
|
+
"dispatches a task and agent completes via done()",
|
|
202
|
+
async () => {
|
|
203
|
+
log("Setting up mock task data on tasksAdapter...");
|
|
204
|
+
|
|
205
|
+
const task: TaskRecord = {
|
|
206
|
+
id: "live-task-1",
|
|
207
|
+
title: "Write a greeting",
|
|
208
|
+
content:
|
|
209
|
+
'Create a file called greeting.txt with the text "Hello World". ' +
|
|
210
|
+
'Then call the "done" MCP tool with status="completed" and summary="Created greeting.txt".',
|
|
211
|
+
status: "open",
|
|
212
|
+
tags: ["auto"],
|
|
213
|
+
priority: 3,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Override tasksAdapter methods to serve our test task
|
|
217
|
+
const tasksAdapter = system.tasksAdapter;
|
|
218
|
+
(tasksAdapter as any).queryReady = vi
|
|
219
|
+
.fn()
|
|
220
|
+
.mockResolvedValueOnce([task])
|
|
221
|
+
.mockResolvedValue([]); // Empty after first poll
|
|
222
|
+
(tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
|
|
223
|
+
(tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
|
|
224
|
+
(tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
|
|
225
|
+
(tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
|
|
226
|
+
|
|
227
|
+
log("Triggering dispatch...");
|
|
228
|
+
await dispatcher.dispatchNow();
|
|
229
|
+
await sleep(2_000);
|
|
230
|
+
|
|
231
|
+
log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
|
|
232
|
+
expect(dispatcher.tracker.activeCount()).toBe(1);
|
|
233
|
+
|
|
234
|
+
const active = dispatcher.tracker.listActive();
|
|
235
|
+
const agentId = active[0]?.agentId;
|
|
236
|
+
log(`Dispatched agent: ${agentId}`);
|
|
237
|
+
expect(agentId).toBeDefined();
|
|
238
|
+
|
|
239
|
+
// Verify agent is running
|
|
240
|
+
const agentRecord = system.agentStore.getAgent(agentId);
|
|
241
|
+
expect(agentRecord).not.toBeNull();
|
|
242
|
+
expect(agentRecord!.state).toBe("running");
|
|
243
|
+
|
|
244
|
+
// Verify it was spawned parentless (root agent)
|
|
245
|
+
expect(agentRecord!.parent_id).toBeNull();
|
|
246
|
+
|
|
247
|
+
// Verify task was claimed
|
|
248
|
+
expect((tasksAdapter as any).assignTask).toHaveBeenCalledWith(
|
|
249
|
+
"live-task-1",
|
|
250
|
+
expect.any(String)
|
|
251
|
+
);
|
|
252
|
+
expect((tasksAdapter as any).transitionTask).toHaveBeenCalledWith(
|
|
253
|
+
"live-task-1",
|
|
254
|
+
"start"
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Now prompt the agent to do its work and call done
|
|
258
|
+
log("Waiting for agent to complete...");
|
|
259
|
+
const result = await system.agentManager.promptUntilDone(
|
|
260
|
+
agentId,
|
|
261
|
+
'Complete your task: create greeting.txt with "Hello World", then call done(status="completed", summary="Created greeting.txt").',
|
|
262
|
+
{
|
|
263
|
+
maxFollowUps: 3,
|
|
264
|
+
onUpdate: (update: any) => {
|
|
265
|
+
if (update.sessionUpdate === "tool_call") {
|
|
266
|
+
log(` [tool_call] ${update.title ?? "unknown"}`);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
log(
|
|
273
|
+
`promptUntilDone result: doneCalled=${result.doneCalled}, status=${result.doneStatus}`
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Wait for lifecycle listener to process the stop event
|
|
277
|
+
await waitFor(
|
|
278
|
+
() => system.agentStore.getAgent(agentId)?.state === "stopped",
|
|
279
|
+
15_000,
|
|
280
|
+
500
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Verify the lifecycle listener detected completion
|
|
284
|
+
log(`Tracker active after done: ${dispatcher.tracker.activeCount()}`);
|
|
285
|
+
log(`Tracker retries: ${dispatcher.tracker.listRetries().length}`);
|
|
286
|
+
|
|
287
|
+
if (result.doneCalled && result.doneStatus === "completed") {
|
|
288
|
+
// Agent called done(completed) → lifecycle listener should have
|
|
289
|
+
// completed the task in the tracker
|
|
290
|
+
const stillTracked = dispatcher.tracker.isTracked("live-task-1");
|
|
291
|
+
log(`Task still tracked: ${stillTracked}`);
|
|
292
|
+
|
|
293
|
+
// The agent should be stopped now
|
|
294
|
+
const finalRecord = system.agentStore.getAgent(agentId);
|
|
295
|
+
log(`Agent final state: ${finalRecord?.state}`);
|
|
296
|
+
expect(finalRecord?.state).toBe("stopped");
|
|
297
|
+
|
|
298
|
+
// Verify greeting.txt was created
|
|
299
|
+
const greetingPath = path.join(testRepo.path, "greeting.txt");
|
|
300
|
+
if (fs.existsSync(greetingPath)) {
|
|
301
|
+
const content = fs.readFileSync(greetingPath, "utf-8");
|
|
302
|
+
log(`greeting.txt content: ${content.trim()}`);
|
|
303
|
+
expect(content).toContain("Hello World");
|
|
304
|
+
} else {
|
|
305
|
+
log("greeting.txt not found (agent may have written elsewhere)");
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
log("Agent did not call done(completed) — checking alternative outcomes");
|
|
309
|
+
// Agent may have been terminated or may still be running
|
|
310
|
+
// This is acceptable in live tests where LLM behavior varies
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
TIMEOUT.MULTI
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// ── Dispatch respects concurrency with real agents ──────────
|
|
317
|
+
|
|
318
|
+
it(
|
|
319
|
+
"dispatches multiple tasks respecting concurrency",
|
|
320
|
+
async () => {
|
|
321
|
+
log("Setting up 3 tasks with concurrency limit of 2...");
|
|
322
|
+
|
|
323
|
+
// Recreate tracker with global limit of 2
|
|
324
|
+
// (We can't change the existing tracker's config, but we
|
|
325
|
+
// can verify the strategy respects the concurrency config
|
|
326
|
+
// it was initialized with — which is global: 3)
|
|
327
|
+
|
|
328
|
+
const tasks: TaskRecord[] = [
|
|
329
|
+
{
|
|
330
|
+
id: "multi-1",
|
|
331
|
+
title: "Task one",
|
|
332
|
+
content: 'Say "Task one done" and call done(status="completed").',
|
|
333
|
+
status: "open",
|
|
334
|
+
tags: ["auto"],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "multi-2",
|
|
338
|
+
title: "Task two",
|
|
339
|
+
content: 'Say "Task two done" and call done(status="completed").',
|
|
340
|
+
status: "open",
|
|
341
|
+
tags: ["auto"],
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
const tasksAdapter = system.tasksAdapter;
|
|
346
|
+
(tasksAdapter as any).queryReady = vi.fn().mockResolvedValue(tasks);
|
|
347
|
+
(tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
|
|
348
|
+
(tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
|
|
349
|
+
(tasksAdapter as any).getTask = vi.fn().mockImplementation(async (id: string) =>
|
|
350
|
+
tasks.find((t) => t.id === id) ?? { id, title: "Unknown", status: "open" }
|
|
351
|
+
);
|
|
352
|
+
(tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
|
|
353
|
+
|
|
354
|
+
log("Triggering dispatch...");
|
|
355
|
+
await dispatcher.dispatchNow();
|
|
356
|
+
await sleep(3_000);
|
|
357
|
+
|
|
358
|
+
log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
|
|
359
|
+
log(`Agents spawned: ${dispatcher.tracker.listActive().map((d) => d.agentId).flat().join(", ")}`);
|
|
360
|
+
|
|
361
|
+
// Both tasks should be dispatched (within global limit of 3)
|
|
362
|
+
expect(dispatcher.tracker.activeCount()).toBe(2);
|
|
363
|
+
expect((tasksAdapter as any).assignTask).toHaveBeenCalledTimes(2);
|
|
364
|
+
|
|
365
|
+
// Verify both agents are running
|
|
366
|
+
const active = dispatcher.tracker.listActive();
|
|
367
|
+
for (const record of active) {
|
|
368
|
+
const agentId = record.agentId;
|
|
369
|
+
const agentRecord = system.agentStore.getAgent(agentId);
|
|
370
|
+
expect(agentRecord).not.toBeNull();
|
|
371
|
+
expect(agentRecord!.state).toBe("running");
|
|
372
|
+
log(`Agent ${agentId} is ${agentRecord!.state}`);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
TIMEOUT.DISPATCH
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// ── Lifecycle listener tracks agent termination ─────────────
|
|
379
|
+
|
|
380
|
+
it(
|
|
381
|
+
"lifecycle listener detects external agent termination",
|
|
382
|
+
async () => {
|
|
383
|
+
log("Dispatching a task...");
|
|
384
|
+
|
|
385
|
+
const task: TaskRecord = {
|
|
386
|
+
id: "terminate-task",
|
|
387
|
+
title: "Long running task",
|
|
388
|
+
content: "Wait for instructions. Do not call done().",
|
|
389
|
+
status: "open",
|
|
390
|
+
tags: ["auto"],
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const tasksAdapter = system.tasksAdapter;
|
|
394
|
+
(tasksAdapter as any).queryReady = vi
|
|
395
|
+
.fn()
|
|
396
|
+
.mockResolvedValueOnce([task])
|
|
397
|
+
.mockResolvedValue([]);
|
|
398
|
+
(tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
|
|
399
|
+
(tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
|
|
400
|
+
(tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
|
|
401
|
+
(tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
|
|
402
|
+
|
|
403
|
+
log("Triggering dispatch...");
|
|
404
|
+
await dispatcher.dispatchNow();
|
|
405
|
+
await sleep(2_000);
|
|
406
|
+
|
|
407
|
+
expect(dispatcher.tracker.activeCount()).toBe(1);
|
|
408
|
+
const agentId = dispatcher.tracker.listActive()[0].agentId;
|
|
409
|
+
log(`Agent spawned: ${agentId}`);
|
|
410
|
+
|
|
411
|
+
log("Terminating agent externally...");
|
|
412
|
+
await system.agentManager.terminate(agentId, "cancelled");
|
|
413
|
+
|
|
414
|
+
// Wait for lifecycle listener to process
|
|
415
|
+
await waitFor(
|
|
416
|
+
() => dispatcher.tracker.activeCount() === 0,
|
|
417
|
+
15_000,
|
|
418
|
+
500
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
log(`Tracker active after terminate: ${dispatcher.tracker.activeCount()}`);
|
|
422
|
+
log(`Tracker retries: ${dispatcher.tracker.listRetries().length}`);
|
|
423
|
+
|
|
424
|
+
// Agent was cancelled (not "completed") → lifecycle listener
|
|
425
|
+
// should have called dispatcher.tracker.fail(), which queues a retry
|
|
426
|
+
// (since maxRetries > 0 and this is attempt 0)
|
|
427
|
+
const isTracked = dispatcher.tracker.isTracked("terminate-task");
|
|
428
|
+
log(`Task still tracked (in retry queue): ${isTracked}`);
|
|
429
|
+
expect(isTracked).toBe(true);
|
|
430
|
+
expect(dispatcher.tracker.listRetries()).toHaveLength(1);
|
|
431
|
+
expect(dispatcher.tracker.listRetries()[0].taskId).toBe("terminate-task");
|
|
432
|
+
},
|
|
433
|
+
TIMEOUT.DISPATCH
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// ── Reconciliation detects closed task ──────────────────────
|
|
437
|
+
|
|
438
|
+
it(
|
|
439
|
+
"reconciliation terminates agent when task is closed externally",
|
|
440
|
+
async () => {
|
|
441
|
+
log("Dispatching a task...");
|
|
442
|
+
|
|
443
|
+
const task: TaskRecord = {
|
|
444
|
+
id: "reconcile-task",
|
|
445
|
+
title: "Task to be closed externally",
|
|
446
|
+
content: "Wait for instructions.",
|
|
447
|
+
status: "open",
|
|
448
|
+
tags: ["auto"],
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const tasksAdapter = system.tasksAdapter;
|
|
452
|
+
(tasksAdapter as any).queryReady = vi
|
|
453
|
+
.fn()
|
|
454
|
+
.mockResolvedValueOnce([task])
|
|
455
|
+
.mockResolvedValue([]);
|
|
456
|
+
(tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
|
|
457
|
+
(tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
|
|
458
|
+
// getTask returns open initially (dispatch doesn't call getTask for new tasks,
|
|
459
|
+
// only for retries — so this won't be consumed during dispatch)
|
|
460
|
+
(tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
|
|
461
|
+
(tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
|
|
462
|
+
|
|
463
|
+
log("Dispatching...");
|
|
464
|
+
await dispatcher.dispatchNow();
|
|
465
|
+
await sleep(2_000);
|
|
466
|
+
|
|
467
|
+
expect(dispatcher.tracker.activeCount()).toBe(1);
|
|
468
|
+
const agentId = dispatcher.tracker.listActive()[0].agentId;
|
|
469
|
+
log(`Agent spawned: ${agentId}`);
|
|
470
|
+
expect(system.agentStore.getAgent(agentId)!.state).toBe("running");
|
|
471
|
+
|
|
472
|
+
// Simulate external state change: task closed
|
|
473
|
+
(tasksAdapter as any).getTask = vi.fn().mockResolvedValue({
|
|
474
|
+
...task,
|
|
475
|
+
status: "closed",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
log("Triggering reconciliation (task now closed externally)...");
|
|
479
|
+
await dispatcher.reconcileNow();
|
|
480
|
+
await waitFor(
|
|
481
|
+
() => system.agentStore.getAgent(agentId)?.state === "stopped",
|
|
482
|
+
15_000,
|
|
483
|
+
500
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
log(`Tracker active after reconcile: ${dispatcher.tracker.activeCount()}`);
|
|
487
|
+
|
|
488
|
+
// Task should be removed from tracker (completed, not retried)
|
|
489
|
+
expect(dispatcher.tracker.isTracked("reconcile-task")).toBe(false);
|
|
490
|
+
expect(dispatcher.tracker.activeCount()).toBe(0);
|
|
491
|
+
|
|
492
|
+
// Agent should be stopped
|
|
493
|
+
const finalRecord = system.agentStore.getAgent(agentId);
|
|
494
|
+
log(`Agent final state: ${finalRecord?.state}`);
|
|
495
|
+
expect(finalRecord?.state).toBe("stopped");
|
|
496
|
+
},
|
|
497
|
+
TIMEOUT.DISPATCH
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// ── Prompt pipeline produces correct prompt ─────────────────
|
|
501
|
+
|
|
502
|
+
it(
|
|
503
|
+
"dispatched agent receives prompt built from task metadata",
|
|
504
|
+
async () => {
|
|
505
|
+
log("Dispatching task with rich metadata...");
|
|
506
|
+
|
|
507
|
+
const task: TaskRecord = {
|
|
508
|
+
id: "prompt-task",
|
|
509
|
+
title: "Fix authentication bug",
|
|
510
|
+
content: "The login form fails when password contains special characters.",
|
|
511
|
+
status: "open",
|
|
512
|
+
tags: ["backend", "auth"],
|
|
513
|
+
priority: 5,
|
|
514
|
+
metadata: {
|
|
515
|
+
criteria: ["Login works with special chars", "No regression in tests"],
|
|
516
|
+
files: ["src/auth.ts", "src/login.tsx"],
|
|
517
|
+
sourceUrl: "https://linear.app/team/issue/AUTH-42",
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const tasksAdapter = system.tasksAdapter;
|
|
522
|
+
(tasksAdapter as any).queryReady = vi
|
|
523
|
+
.fn()
|
|
524
|
+
.mockResolvedValueOnce([task])
|
|
525
|
+
.mockResolvedValue([]);
|
|
526
|
+
(tasksAdapter as any).assignTask = vi.fn().mockResolvedValue(undefined);
|
|
527
|
+
(tasksAdapter as any).transitionTask = vi.fn().mockResolvedValue(undefined);
|
|
528
|
+
(tasksAdapter as any).getTask = vi.fn().mockResolvedValue(task);
|
|
529
|
+
(tasksAdapter as any).listTasks = vi.fn().mockResolvedValue([]);
|
|
530
|
+
|
|
531
|
+
// Capture the spawn call to inspect the prompt
|
|
532
|
+
const originalSpawn = system.agentManager.spawn.bind(system.agentManager);
|
|
533
|
+
let capturedPrompt = "";
|
|
534
|
+
(system.agentManager as any).spawn = async (opts: any) => {
|
|
535
|
+
capturedPrompt = opts.task;
|
|
536
|
+
return originalSpawn(opts);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
log("Triggering dispatch...");
|
|
540
|
+
await dispatcher.dispatchNow();
|
|
541
|
+
await sleep(2_000);
|
|
542
|
+
|
|
543
|
+
log(`Captured prompt length: ${capturedPrompt.length}`);
|
|
544
|
+
log(`Prompt preview: ${capturedPrompt.substring(0, 200)}...`);
|
|
545
|
+
|
|
546
|
+
// Verify prompt contains task metadata
|
|
547
|
+
expect(capturedPrompt).toContain("## Task: Fix authentication bug");
|
|
548
|
+
expect(capturedPrompt).toContain("login form fails");
|
|
549
|
+
expect(capturedPrompt).toContain("Task ID: prompt-task");
|
|
550
|
+
expect(capturedPrompt).toContain("Tags: backend, auth");
|
|
551
|
+
expect(capturedPrompt).toContain("Priority: 5");
|
|
552
|
+
expect(capturedPrompt).toContain("### Acceptance Criteria");
|
|
553
|
+
expect(capturedPrompt).toContain("Login works with special chars");
|
|
554
|
+
expect(capturedPrompt).toContain("### Relevant Files");
|
|
555
|
+
expect(capturedPrompt).toContain("src/auth.ts");
|
|
556
|
+
expect(capturedPrompt).toContain("Source: https://linear.app/team/issue/AUTH-42");
|
|
557
|
+
expect(capturedPrompt).toContain("**worker** role");
|
|
558
|
+
|
|
559
|
+
// Should NOT contain retry context on first attempt
|
|
560
|
+
expect(capturedPrompt).not.toContain("Retry");
|
|
561
|
+
},
|
|
562
|
+
TIMEOUT.DISPATCH
|
|
563
|
+
);
|
|
564
|
+
});
|