opencode-swarm-plugin 0.44.0 → 0.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/swarm.serve.test.ts +6 -4
- package/bin/swarm.ts +16 -10
- package/dist/compaction-prompt-scoring.js +139 -0
- package/dist/eval-capture.js +12811 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.js +7644 -62599
- package/dist/plugin.js +23766 -78721
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/package.json +17 -5
- package/.changeset/swarm-insights-data-layer.md +0 -63
- package/.hive/analysis/eval-failure-analysis-2025-12-25.md +0 -331
- package/.hive/analysis/session-data-quality-audit.md +0 -320
- package/.hive/eval-results.json +0 -483
- package/.hive/issues.jsonl +0 -138
- package/.hive/memories.jsonl +0 -729
- package/.opencode/eval-history.jsonl +0 -327
- package/.turbo/turbo-build.log +0 -9
- package/CHANGELOG.md +0 -2286
- package/SCORER-ANALYSIS.md +0 -598
- package/docs/analysis/subagent-coordination-patterns.md +0 -902
- package/docs/analysis-socratic-planner-pattern.md +0 -504
- package/docs/planning/ADR-001-monorepo-structure.md +0 -171
- package/docs/planning/ADR-002-package-extraction.md +0 -393
- package/docs/planning/ADR-003-performance-improvements.md +0 -451
- package/docs/planning/ADR-004-message-queue-features.md +0 -187
- package/docs/planning/ADR-005-devtools-observability.md +0 -202
- package/docs/planning/ADR-007-swarm-enhancements-worktree-review.md +0 -168
- package/docs/planning/ADR-008-worker-handoff-protocol.md +0 -293
- package/docs/planning/ADR-009-oh-my-opencode-patterns.md +0 -353
- package/docs/planning/ADR-010-cass-inhousing.md +0 -1215
- package/docs/planning/ROADMAP.md +0 -368
- package/docs/semantic-memory-cli-syntax.md +0 -123
- package/docs/swarm-mail-architecture.md +0 -1147
- package/docs/testing/context-recovery-test.md +0 -470
- package/evals/ARCHITECTURE.md +0 -1189
- package/evals/README.md +0 -768
- package/evals/compaction-prompt.eval.ts +0 -149
- package/evals/compaction-resumption.eval.ts +0 -289
- package/evals/coordinator-behavior.eval.ts +0 -307
- package/evals/coordinator-session.eval.ts +0 -154
- package/evals/evalite.config.ts.bak +0 -15
- package/evals/example.eval.ts +0 -31
- package/evals/fixtures/cass-baseline.ts +0 -217
- package/evals/fixtures/compaction-cases.ts +0 -350
- package/evals/fixtures/compaction-prompt-cases.ts +0 -311
- package/evals/fixtures/coordinator-sessions.ts +0 -328
- package/evals/fixtures/decomposition-cases.ts +0 -105
- package/evals/lib/compaction-loader.test.ts +0 -248
- package/evals/lib/compaction-loader.ts +0 -320
- package/evals/lib/data-loader.evalite-test.ts +0 -289
- package/evals/lib/data-loader.test.ts +0 -345
- package/evals/lib/data-loader.ts +0 -281
- package/evals/lib/llm.ts +0 -115
- package/evals/scorers/compaction-prompt-scorers.ts +0 -145
- package/evals/scorers/compaction-scorers.ts +0 -305
- package/evals/scorers/coordinator-discipline.evalite-test.ts +0 -539
- package/evals/scorers/coordinator-discipline.ts +0 -325
- package/evals/scorers/index.test.ts +0 -146
- package/evals/scorers/index.ts +0 -328
- package/evals/scorers/outcome-scorers.evalite-test.ts +0 -27
- package/evals/scorers/outcome-scorers.ts +0 -349
- package/evals/swarm-decomposition.eval.ts +0 -121
- package/examples/commands/swarm.md +0 -745
- package/examples/plugin-wrapper-template.ts +0 -2515
- package/examples/skills/hive-workflow/SKILL.md +0 -212
- package/examples/skills/skill-creator/SKILL.md +0 -223
- package/examples/skills/swarm-coordination/SKILL.md +0 -292
- package/global-skills/cli-builder/SKILL.md +0 -344
- package/global-skills/cli-builder/references/advanced-patterns.md +0 -244
- package/global-skills/learning-systems/SKILL.md +0 -644
- package/global-skills/skill-creator/LICENSE.txt +0 -202
- package/global-skills/skill-creator/SKILL.md +0 -352
- package/global-skills/skill-creator/references/output-patterns.md +0 -82
- package/global-skills/skill-creator/references/workflows.md +0 -28
- package/global-skills/swarm-coordination/SKILL.md +0 -995
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +0 -235
- package/global-skills/swarm-coordination/references/strategies.md +0 -138
- package/global-skills/system-design/SKILL.md +0 -213
- package/global-skills/testing-patterns/SKILL.md +0 -430
- package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +0 -586
- package/opencode-swarm-plugin-0.30.7.tgz +0 -0
- package/opencode-swarm-plugin-0.31.0.tgz +0 -0
- package/scripts/cleanup-test-memories.ts +0 -346
- package/scripts/init-skill.ts +0 -222
- package/scripts/migrate-unknown-sessions.ts +0 -349
- package/scripts/validate-skill.ts +0 -204
- package/src/agent-mail.ts +0 -1724
- package/src/anti-patterns.test.ts +0 -1167
- package/src/anti-patterns.ts +0 -448
- package/src/compaction-capture.integration.test.ts +0 -257
- package/src/compaction-hook.test.ts +0 -838
- package/src/compaction-hook.ts +0 -1204
- package/src/compaction-observability.integration.test.ts +0 -139
- package/src/compaction-observability.test.ts +0 -187
- package/src/compaction-observability.ts +0 -324
- package/src/compaction-prompt-scorers.test.ts +0 -475
- package/src/compaction-prompt-scoring.ts +0 -300
- package/src/contributor-tools.test.ts +0 -133
- package/src/contributor-tools.ts +0 -201
- package/src/dashboard.test.ts +0 -611
- package/src/dashboard.ts +0 -462
- package/src/error-enrichment.test.ts +0 -403
- package/src/error-enrichment.ts +0 -219
- package/src/eval-capture.test.ts +0 -1015
- package/src/eval-capture.ts +0 -929
- package/src/eval-gates.test.ts +0 -306
- package/src/eval-gates.ts +0 -218
- package/src/eval-history.test.ts +0 -508
- package/src/eval-history.ts +0 -214
- package/src/eval-learning.test.ts +0 -378
- package/src/eval-learning.ts +0 -360
- package/src/eval-runner.test.ts +0 -223
- package/src/eval-runner.ts +0 -402
- package/src/export-tools.test.ts +0 -476
- package/src/export-tools.ts +0 -257
- package/src/hive.integration.test.ts +0 -2241
- package/src/hive.ts +0 -1628
- package/src/index.ts +0 -940
- package/src/learning.integration.test.ts +0 -1815
- package/src/learning.ts +0 -1079
- package/src/logger.test.ts +0 -189
- package/src/logger.ts +0 -135
- package/src/mandate-promotion.test.ts +0 -473
- package/src/mandate-promotion.ts +0 -239
- package/src/mandate-storage.integration.test.ts +0 -601
- package/src/mandate-storage.test.ts +0 -578
- package/src/mandate-storage.ts +0 -794
- package/src/mandates.ts +0 -540
- package/src/memory-tools.test.ts +0 -195
- package/src/memory-tools.ts +0 -344
- package/src/memory.integration.test.ts +0 -334
- package/src/memory.test.ts +0 -158
- package/src/memory.ts +0 -527
- package/src/model-selection.test.ts +0 -188
- package/src/model-selection.ts +0 -68
- package/src/observability-tools.test.ts +0 -359
- package/src/observability-tools.ts +0 -871
- package/src/output-guardrails.test.ts +0 -438
- package/src/output-guardrails.ts +0 -381
- package/src/pattern-maturity.test.ts +0 -1160
- package/src/pattern-maturity.ts +0 -525
- package/src/planning-guardrails.test.ts +0 -491
- package/src/planning-guardrails.ts +0 -438
- package/src/plugin.ts +0 -23
- package/src/post-compaction-tracker.test.ts +0 -251
- package/src/post-compaction-tracker.ts +0 -237
- package/src/query-tools.test.ts +0 -636
- package/src/query-tools.ts +0 -324
- package/src/rate-limiter.integration.test.ts +0 -466
- package/src/rate-limiter.ts +0 -774
- package/src/replay-tools.test.ts +0 -496
- package/src/replay-tools.ts +0 -240
- package/src/repo-crawl.integration.test.ts +0 -441
- package/src/repo-crawl.ts +0 -610
- package/src/schemas/cell-events.test.ts +0 -347
- package/src/schemas/cell-events.ts +0 -807
- package/src/schemas/cell.ts +0 -257
- package/src/schemas/evaluation.ts +0 -166
- package/src/schemas/index.test.ts +0 -199
- package/src/schemas/index.ts +0 -286
- package/src/schemas/mandate.ts +0 -232
- package/src/schemas/swarm-context.ts +0 -115
- package/src/schemas/task.ts +0 -161
- package/src/schemas/worker-handoff.test.ts +0 -302
- package/src/schemas/worker-handoff.ts +0 -131
- package/src/sessions/agent-discovery.test.ts +0 -137
- package/src/sessions/agent-discovery.ts +0 -112
- package/src/sessions/index.ts +0 -15
- package/src/skills.integration.test.ts +0 -1192
- package/src/skills.test.ts +0 -643
- package/src/skills.ts +0 -1549
- package/src/storage.integration.test.ts +0 -341
- package/src/storage.ts +0 -884
- package/src/structured.integration.test.ts +0 -817
- package/src/structured.test.ts +0 -1046
- package/src/structured.ts +0 -762
- package/src/swarm-decompose.test.ts +0 -188
- package/src/swarm-decompose.ts +0 -1302
- package/src/swarm-deferred.integration.test.ts +0 -157
- package/src/swarm-deferred.test.ts +0 -38
- package/src/swarm-insights.test.ts +0 -214
- package/src/swarm-insights.ts +0 -459
- package/src/swarm-mail.integration.test.ts +0 -970
- package/src/swarm-mail.ts +0 -739
- package/src/swarm-orchestrate.integration.test.ts +0 -282
- package/src/swarm-orchestrate.test.ts +0 -548
- package/src/swarm-orchestrate.ts +0 -3084
- package/src/swarm-prompts.test.ts +0 -1270
- package/src/swarm-prompts.ts +0 -2077
- package/src/swarm-research.integration.test.ts +0 -701
- package/src/swarm-research.test.ts +0 -698
- package/src/swarm-research.ts +0 -472
- package/src/swarm-review.integration.test.ts +0 -285
- package/src/swarm-review.test.ts +0 -879
- package/src/swarm-review.ts +0 -709
- package/src/swarm-strategies.ts +0 -407
- package/src/swarm-worktree.test.ts +0 -501
- package/src/swarm-worktree.ts +0 -575
- package/src/swarm.integration.test.ts +0 -2377
- package/src/swarm.ts +0 -38
- package/src/tool-adapter.integration.test.ts +0 -1221
- package/src/tool-availability.ts +0 -461
- package/tsconfig.json +0 -28
|
@@ -1,2377 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Swarm Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* These tests require:
|
|
5
|
-
* - beads CLI installed and configured
|
|
6
|
-
* - Agent Mail server running at AGENT_MAIL_URL (default: http://agent-mail:8765 in Docker)
|
|
7
|
-
*
|
|
8
|
-
* Run with: pnpm test:integration (or docker:test for full Docker environment)
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
11
|
-
import {
|
|
12
|
-
swarm_decompose,
|
|
13
|
-
swarm_validate_decomposition,
|
|
14
|
-
swarm_status,
|
|
15
|
-
swarm_progress,
|
|
16
|
-
swarm_complete,
|
|
17
|
-
swarm_subtask_prompt,
|
|
18
|
-
swarm_spawn_subtask,
|
|
19
|
-
swarm_evaluation_prompt,
|
|
20
|
-
swarm_select_strategy,
|
|
21
|
-
swarm_plan_prompt,
|
|
22
|
-
formatSubtaskPromptV2,
|
|
23
|
-
SUBTASK_PROMPT_V2,
|
|
24
|
-
swarm_checkpoint,
|
|
25
|
-
swarm_recover,
|
|
26
|
-
} from "./swarm";
|
|
27
|
-
import { swarm_review, swarm_review_feedback } from "./swarm-review";
|
|
28
|
-
import { mcpCall, setState, clearState, AGENT_MAIL_URL } from "./agent-mail";
|
|
29
|
-
|
|
30
|
-
// ============================================================================
|
|
31
|
-
// Test Configuration
|
|
32
|
-
// ============================================================================
|
|
33
|
-
|
|
34
|
-
const TEST_SESSION_ID = `test-swarm-${Date.now()}`;
|
|
35
|
-
const TEST_PROJECT_PATH = `/tmp/test-swarm-${Date.now()}`;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Mock tool context for execute functions.
|
|
39
|
-
* The real context is provided by OpenCode runtime.
|
|
40
|
-
*/
|
|
41
|
-
const mockContext = {
|
|
42
|
-
sessionID: TEST_SESSION_ID,
|
|
43
|
-
messageID: `test-message-${Date.now()}`,
|
|
44
|
-
agent: "test-agent",
|
|
45
|
-
abort: new AbortController().signal,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Check if Agent Mail is available
|
|
50
|
-
*/
|
|
51
|
-
async function isAgentMailAvailable(): Promise<boolean> {
|
|
52
|
-
try {
|
|
53
|
-
const url = process.env.AGENT_MAIL_URL || AGENT_MAIL_URL;
|
|
54
|
-
const response = await fetch(`${url}/health/liveness`);
|
|
55
|
-
return response.ok;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if beads CLI is available
|
|
63
|
-
*/
|
|
64
|
-
async function isBeadsAvailable(): Promise<boolean> {
|
|
65
|
-
try {
|
|
66
|
-
const result = await Bun.$`bd --version`.quiet().nothrow();
|
|
67
|
-
return result.exitCode === 0;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Prompt Generation Tests (No external dependencies)
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
describe("swarm_decompose", () => {
|
|
78
|
-
it("generates valid decomposition prompt", async () => {
|
|
79
|
-
const result = await swarm_decompose.execute(
|
|
80
|
-
{
|
|
81
|
-
task: "Add user authentication with OAuth",
|
|
82
|
-
},
|
|
83
|
-
mockContext,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const parsed = JSON.parse(result);
|
|
87
|
-
|
|
88
|
-
expect(parsed).toHaveProperty("prompt");
|
|
89
|
-
expect(parsed).toHaveProperty("expected_schema", "CellTree");
|
|
90
|
-
expect(parsed).toHaveProperty("schema_hint");
|
|
91
|
-
expect(parsed.prompt).toContain("Add user authentication with OAuth");
|
|
92
|
-
expect(parsed.prompt).toContain("as many as needed");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("includes context in prompt when provided", async () => {
|
|
96
|
-
const result = await swarm_decompose.execute(
|
|
97
|
-
{
|
|
98
|
-
task: "Refactor the API routes",
|
|
99
|
-
context: "Using Next.js App Router with RSC",
|
|
100
|
-
},
|
|
101
|
-
mockContext,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const parsed = JSON.parse(result);
|
|
105
|
-
|
|
106
|
-
expect(parsed.prompt).toContain("Using Next.js App Router with RSC");
|
|
107
|
-
expect(parsed.prompt).toContain("Additional Context");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// ============================================================================
|
|
113
|
-
// Strategy Selection Tests
|
|
114
|
-
// ============================================================================
|
|
115
|
-
|
|
116
|
-
describe("swarm_select_strategy", () => {
|
|
117
|
-
it("selects feature-based for 'add' tasks", async () => {
|
|
118
|
-
const result = await swarm_select_strategy.execute(
|
|
119
|
-
{
|
|
120
|
-
task: "Add user authentication with OAuth",
|
|
121
|
-
},
|
|
122
|
-
mockContext,
|
|
123
|
-
);
|
|
124
|
-
const parsed = JSON.parse(result);
|
|
125
|
-
|
|
126
|
-
expect(parsed.strategy).toBe("feature-based");
|
|
127
|
-
expect(parsed.confidence).toBeGreaterThan(0.5);
|
|
128
|
-
expect(parsed.reasoning).toContain("add");
|
|
129
|
-
expect(parsed.guidelines).toBeInstanceOf(Array);
|
|
130
|
-
expect(parsed.anti_patterns).toBeInstanceOf(Array);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("selects file-based for 'refactor' tasks", async () => {
|
|
134
|
-
const result = await swarm_select_strategy.execute(
|
|
135
|
-
{
|
|
136
|
-
task: "Refactor all components to use new API",
|
|
137
|
-
},
|
|
138
|
-
mockContext,
|
|
139
|
-
);
|
|
140
|
-
const parsed = JSON.parse(result);
|
|
141
|
-
|
|
142
|
-
expect(parsed.strategy).toBe("file-based");
|
|
143
|
-
expect(parsed.confidence).toBeGreaterThanOrEqual(0.5);
|
|
144
|
-
expect(parsed.reasoning).toContain("refactor");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("selects risk-based for 'fix security' tasks", async () => {
|
|
148
|
-
const result = await swarm_select_strategy.execute(
|
|
149
|
-
{
|
|
150
|
-
task: "Fix security vulnerability in authentication",
|
|
151
|
-
},
|
|
152
|
-
mockContext,
|
|
153
|
-
);
|
|
154
|
-
const parsed = JSON.parse(result);
|
|
155
|
-
|
|
156
|
-
expect(parsed.strategy).toBe("risk-based");
|
|
157
|
-
expect(parsed.confidence).toBeGreaterThan(0.5);
|
|
158
|
-
// Should match either 'fix' or 'security'
|
|
159
|
-
expect(
|
|
160
|
-
parsed.reasoning.includes("fix") || parsed.reasoning.includes("security"),
|
|
161
|
-
).toBe(true);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("defaults to feature-based when no keywords match", async () => {
|
|
165
|
-
const result = await swarm_select_strategy.execute(
|
|
166
|
-
{
|
|
167
|
-
task: "Something completely unrelated without keywords",
|
|
168
|
-
},
|
|
169
|
-
mockContext,
|
|
170
|
-
);
|
|
171
|
-
const parsed = JSON.parse(result);
|
|
172
|
-
|
|
173
|
-
expect(parsed.strategy).toBe("feature-based");
|
|
174
|
-
// Confidence should be lower without keyword matches
|
|
175
|
-
expect(parsed.confidence).toBeLessThanOrEqual(0.6);
|
|
176
|
-
expect(parsed.reasoning).toContain("Defaulting to feature-based");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("includes confidence score and reasoning", async () => {
|
|
180
|
-
const result = await swarm_select_strategy.execute(
|
|
181
|
-
{
|
|
182
|
-
task: "Implement new dashboard feature",
|
|
183
|
-
},
|
|
184
|
-
mockContext,
|
|
185
|
-
);
|
|
186
|
-
const parsed = JSON.parse(result);
|
|
187
|
-
|
|
188
|
-
expect(parsed).toHaveProperty("strategy");
|
|
189
|
-
expect(parsed).toHaveProperty("confidence");
|
|
190
|
-
expect(parsed).toHaveProperty("reasoning");
|
|
191
|
-
expect(parsed).toHaveProperty("description");
|
|
192
|
-
expect(typeof parsed.confidence).toBe("number");
|
|
193
|
-
expect(parsed.confidence).toBeGreaterThanOrEqual(0);
|
|
194
|
-
expect(parsed.confidence).toBeLessThanOrEqual(1);
|
|
195
|
-
expect(typeof parsed.reasoning).toBe("string");
|
|
196
|
-
expect(parsed.reasoning.length).toBeGreaterThan(0);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("includes alternative strategies with scores", async () => {
|
|
200
|
-
const result = await swarm_select_strategy.execute(
|
|
201
|
-
{
|
|
202
|
-
task: "Build new payment processing module",
|
|
203
|
-
},
|
|
204
|
-
mockContext,
|
|
205
|
-
);
|
|
206
|
-
const parsed = JSON.parse(result);
|
|
207
|
-
|
|
208
|
-
expect(parsed).toHaveProperty("alternatives");
|
|
209
|
-
expect(parsed.alternatives).toBeInstanceOf(Array);
|
|
210
|
-
expect(parsed.alternatives.length).toBe(3); // 4 strategies - 1 selected = 3 alternatives
|
|
211
|
-
|
|
212
|
-
for (const alt of parsed.alternatives) {
|
|
213
|
-
expect(alt).toHaveProperty("strategy");
|
|
214
|
-
expect(alt).toHaveProperty("description");
|
|
215
|
-
expect(alt).toHaveProperty("score");
|
|
216
|
-
expect([
|
|
217
|
-
"file-based",
|
|
218
|
-
"feature-based",
|
|
219
|
-
"risk-based",
|
|
220
|
-
"research-based",
|
|
221
|
-
]).toContain(alt.strategy);
|
|
222
|
-
expect(typeof alt.score).toBe("number");
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("includes codebase context in reasoning when provided", async () => {
|
|
227
|
-
const result = await swarm_select_strategy.execute(
|
|
228
|
-
{
|
|
229
|
-
task: "Add new API endpoint",
|
|
230
|
-
codebase_context: "Using Express.js with TypeScript and PostgreSQL",
|
|
231
|
-
},
|
|
232
|
-
mockContext,
|
|
233
|
-
);
|
|
234
|
-
const parsed = JSON.parse(result);
|
|
235
|
-
|
|
236
|
-
expect(parsed.reasoning).toContain("Express.js");
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// Planning Prompt Tests
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
|
-
describe("swarm_plan_prompt", () => {
|
|
245
|
-
it("auto-selects strategy when not specified", async () => {
|
|
246
|
-
const result = await swarm_plan_prompt.execute(
|
|
247
|
-
{
|
|
248
|
-
task: "Add user settings page",
|
|
249
|
-
query_cass: false, // Disable CASS to isolate test
|
|
250
|
-
},
|
|
251
|
-
mockContext,
|
|
252
|
-
);
|
|
253
|
-
const parsed = JSON.parse(result);
|
|
254
|
-
|
|
255
|
-
expect(parsed).toHaveProperty("prompt");
|
|
256
|
-
expect(parsed).toHaveProperty("strategy");
|
|
257
|
-
expect(parsed.strategy).toHaveProperty("selected");
|
|
258
|
-
expect(parsed.strategy).toHaveProperty("reasoning");
|
|
259
|
-
expect(parsed.strategy.selected).toBe("feature-based"); // 'add' keyword
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("uses explicit strategy when provided", async () => {
|
|
263
|
-
const result = await swarm_plan_prompt.execute(
|
|
264
|
-
{
|
|
265
|
-
task: "Do something",
|
|
266
|
-
strategy: "risk-based",
|
|
267
|
-
query_cass: false,
|
|
268
|
-
},
|
|
269
|
-
mockContext,
|
|
270
|
-
);
|
|
271
|
-
const parsed = JSON.parse(result);
|
|
272
|
-
|
|
273
|
-
expect(parsed.strategy.selected).toBe("risk-based");
|
|
274
|
-
expect(parsed.strategy.reasoning).toContain("User-specified strategy");
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("includes strategy guidelines in prompt", async () => {
|
|
278
|
-
const result = await swarm_plan_prompt.execute(
|
|
279
|
-
{
|
|
280
|
-
task: "Refactor the codebase",
|
|
281
|
-
query_cass: false,
|
|
282
|
-
},
|
|
283
|
-
mockContext,
|
|
284
|
-
);
|
|
285
|
-
const parsed = JSON.parse(result);
|
|
286
|
-
|
|
287
|
-
// Prompt should contain strategy-specific guidelines
|
|
288
|
-
expect(parsed.prompt).toContain("## Strategy:");
|
|
289
|
-
expect(parsed.prompt).toContain("### Guidelines");
|
|
290
|
-
expect(parsed.prompt).toContain("### Anti-Patterns");
|
|
291
|
-
expect(parsed.prompt).toContain("### Examples");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("includes anti-patterns in output", async () => {
|
|
295
|
-
const result = await swarm_plan_prompt.execute(
|
|
296
|
-
{
|
|
297
|
-
task: "Build new feature",
|
|
298
|
-
query_cass: false,
|
|
299
|
-
},
|
|
300
|
-
mockContext,
|
|
301
|
-
);
|
|
302
|
-
const parsed = JSON.parse(result);
|
|
303
|
-
|
|
304
|
-
expect(parsed.strategy).toHaveProperty("anti_patterns");
|
|
305
|
-
expect(parsed.strategy.anti_patterns).toBeInstanceOf(Array);
|
|
306
|
-
expect(parsed.strategy.anti_patterns.length).toBeGreaterThan(0);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("returns expected_schema and validation_note", async () => {
|
|
310
|
-
const result = await swarm_plan_prompt.execute(
|
|
311
|
-
{
|
|
312
|
-
task: "Some task",
|
|
313
|
-
query_cass: false,
|
|
314
|
-
},
|
|
315
|
-
mockContext,
|
|
316
|
-
);
|
|
317
|
-
const parsed = JSON.parse(result);
|
|
318
|
-
|
|
319
|
-
expect(parsed).toHaveProperty("expected_schema", "CellTree");
|
|
320
|
-
expect(parsed).toHaveProperty("validation_note");
|
|
321
|
-
expect(parsed.validation_note).toContain("swarm_validate_decomposition");
|
|
322
|
-
expect(parsed).toHaveProperty("schema_hint");
|
|
323
|
-
expect(parsed.schema_hint).toHaveProperty("epic");
|
|
324
|
-
expect(parsed.schema_hint).toHaveProperty("subtasks");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("includes strategy and skills info in output", async () => {
|
|
328
|
-
// Test swarm_plan_prompt output structure
|
|
329
|
-
const result = await swarm_plan_prompt.execute(
|
|
330
|
-
{
|
|
331
|
-
task: "Add feature",
|
|
332
|
-
},
|
|
333
|
-
mockContext,
|
|
334
|
-
);
|
|
335
|
-
const parsed = JSON.parse(result);
|
|
336
|
-
|
|
337
|
-
// Should have strategy info
|
|
338
|
-
expect(parsed).toHaveProperty("strategy");
|
|
339
|
-
expect(parsed.strategy).toHaveProperty("selected");
|
|
340
|
-
expect(parsed.strategy).toHaveProperty("reasoning");
|
|
341
|
-
expect(parsed.strategy).toHaveProperty("guidelines");
|
|
342
|
-
expect(parsed.strategy).toHaveProperty("anti_patterns");
|
|
343
|
-
|
|
344
|
-
// Should have skills info
|
|
345
|
-
expect(parsed).toHaveProperty("skills");
|
|
346
|
-
expect(parsed.skills).toHaveProperty("included");
|
|
347
|
-
|
|
348
|
-
// Should have memory query instruction
|
|
349
|
-
expect(parsed).toHaveProperty("memory_query");
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("includes context in prompt when provided", async () => {
|
|
353
|
-
const result = await swarm_plan_prompt.execute(
|
|
354
|
-
{
|
|
355
|
-
task: "Add user profile",
|
|
356
|
-
context: "We use Next.js App Router with server components",
|
|
357
|
-
query_cass: false,
|
|
358
|
-
},
|
|
359
|
-
mockContext,
|
|
360
|
-
);
|
|
361
|
-
const parsed = JSON.parse(result);
|
|
362
|
-
|
|
363
|
-
expect(parsed.prompt).toContain("Next.js App Router");
|
|
364
|
-
expect(parsed.prompt).toContain("server components");
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("swarm_validate_decomposition", () => {
|
|
370
|
-
it("validates correct CellTree", async () => {
|
|
371
|
-
const validCellTree = JSON.stringify({
|
|
372
|
-
epic: {
|
|
373
|
-
title: "Add OAuth",
|
|
374
|
-
description: "Implement OAuth authentication",
|
|
375
|
-
},
|
|
376
|
-
subtasks: [
|
|
377
|
-
{
|
|
378
|
-
title: "Add OAuth provider config",
|
|
379
|
-
description: "Set up Google OAuth",
|
|
380
|
-
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
381
|
-
dependencies: [],
|
|
382
|
-
estimated_complexity: 2,
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
title: "Add login UI",
|
|
386
|
-
description: "Create login button component",
|
|
387
|
-
files: ["src/components/LoginButton.tsx"],
|
|
388
|
-
dependencies: [0],
|
|
389
|
-
estimated_complexity: 1,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const result = await swarm_validate_decomposition.execute(
|
|
395
|
-
{ response: validCellTree },
|
|
396
|
-
mockContext,
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
const parsed = JSON.parse(result);
|
|
400
|
-
|
|
401
|
-
expect(parsed.valid).toBe(true);
|
|
402
|
-
expect(parsed.cell_tree).toBeDefined();
|
|
403
|
-
expect(parsed.stats).toEqual({
|
|
404
|
-
subtask_count: 2,
|
|
405
|
-
total_files: 3,
|
|
406
|
-
total_complexity: 3,
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it("rejects file conflicts", async () => {
|
|
411
|
-
const conflictingCellTree = JSON.stringify({
|
|
412
|
-
epic: {
|
|
413
|
-
title: "Conflicting files",
|
|
414
|
-
},
|
|
415
|
-
subtasks: [
|
|
416
|
-
{
|
|
417
|
-
title: "Task A",
|
|
418
|
-
files: ["src/shared.ts"],
|
|
419
|
-
dependencies: [],
|
|
420
|
-
estimated_complexity: 1,
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
title: "Task B",
|
|
424
|
-
files: ["src/shared.ts"], // Conflict!
|
|
425
|
-
dependencies: [],
|
|
426
|
-
estimated_complexity: 1,
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
const result = await swarm_validate_decomposition.execute(
|
|
432
|
-
{ response: conflictingCellTree },
|
|
433
|
-
mockContext,
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
const parsed = JSON.parse(result);
|
|
437
|
-
|
|
438
|
-
expect(parsed.valid).toBe(false);
|
|
439
|
-
expect(parsed.error).toContain("File conflicts detected");
|
|
440
|
-
expect(parsed.error).toContain("src/shared.ts");
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("rejects invalid dependencies (forward reference)", async () => {
|
|
444
|
-
const invalidDeps = JSON.stringify({
|
|
445
|
-
epic: {
|
|
446
|
-
title: "Invalid deps",
|
|
447
|
-
},
|
|
448
|
-
subtasks: [
|
|
449
|
-
{
|
|
450
|
-
title: "Task A",
|
|
451
|
-
files: ["src/a.ts"],
|
|
452
|
-
dependencies: [1], // Invalid: depends on later task
|
|
453
|
-
estimated_complexity: 1,
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
title: "Task B",
|
|
457
|
-
files: ["src/b.ts"],
|
|
458
|
-
dependencies: [],
|
|
459
|
-
estimated_complexity: 1,
|
|
460
|
-
},
|
|
461
|
-
],
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
const result = await swarm_validate_decomposition.execute(
|
|
465
|
-
{ response: invalidDeps },
|
|
466
|
-
mockContext,
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
const parsed = JSON.parse(result);
|
|
470
|
-
|
|
471
|
-
expect(parsed.valid).toBe(false);
|
|
472
|
-
expect(parsed.error).toContain("Invalid dependency");
|
|
473
|
-
expect(parsed.hint).toContain("Reorder subtasks");
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("rejects invalid JSON", async () => {
|
|
477
|
-
const result = await swarm_validate_decomposition.execute(
|
|
478
|
-
{ response: "not valid json {" },
|
|
479
|
-
mockContext,
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
const parsed = JSON.parse(result);
|
|
483
|
-
|
|
484
|
-
expect(parsed.valid).toBe(false);
|
|
485
|
-
expect(parsed.error).toContain("Invalid JSON");
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it("rejects missing required fields", async () => {
|
|
489
|
-
const missingFields = JSON.stringify({
|
|
490
|
-
epic: { title: "Missing subtasks" },
|
|
491
|
-
// No subtasks array
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
const result = await swarm_validate_decomposition.execute(
|
|
495
|
-
{ response: missingFields },
|
|
496
|
-
mockContext,
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
const parsed = JSON.parse(result);
|
|
500
|
-
|
|
501
|
-
expect(parsed.valid).toBe(false);
|
|
502
|
-
expect(parsed.error).toContain("Schema validation failed");
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
describe("swarm_subtask_prompt", () => {
|
|
507
|
-
it("generates complete subtask prompt", async () => {
|
|
508
|
-
const result = await swarm_subtask_prompt.execute(
|
|
509
|
-
{
|
|
510
|
-
agent_name: "BlueLake",
|
|
511
|
-
bead_id: "bd-abc123.1",
|
|
512
|
-
epic_id: "bd-abc123",
|
|
513
|
-
subtask_title: "Add OAuth provider",
|
|
514
|
-
subtask_description: "Configure Google OAuth in the auth config",
|
|
515
|
-
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
516
|
-
shared_context: "We are using NextAuth.js v5",
|
|
517
|
-
},
|
|
518
|
-
mockContext,
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
// Result is the prompt string directly
|
|
522
|
-
expect(result).toContain("BlueLake");
|
|
523
|
-
expect(result).toContain("bd-abc123.1");
|
|
524
|
-
expect(result).toContain("bd-abc123");
|
|
525
|
-
expect(result).toContain("Add OAuth provider");
|
|
526
|
-
expect(result).toContain("Configure Google OAuth");
|
|
527
|
-
expect(result).toContain("src/auth/google.ts");
|
|
528
|
-
expect(result).toContain("NextAuth.js v5");
|
|
529
|
-
expect(result).toContain("swarm_progress");
|
|
530
|
-
expect(result).toContain("swarm_complete");
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it("handles missing optional fields", async () => {
|
|
534
|
-
const result = await swarm_subtask_prompt.execute(
|
|
535
|
-
{
|
|
536
|
-
agent_name: "RedStone",
|
|
537
|
-
bead_id: "bd-xyz789.2",
|
|
538
|
-
epic_id: "bd-xyz789",
|
|
539
|
-
subtask_title: "Simple task",
|
|
540
|
-
files: [],
|
|
541
|
-
},
|
|
542
|
-
mockContext,
|
|
543
|
-
);
|
|
544
|
-
|
|
545
|
-
expect(result).toContain("RedStone");
|
|
546
|
-
expect(result).toContain("bd-xyz789.2");
|
|
547
|
-
expect(result).toContain("Simple task");
|
|
548
|
-
expect(result).toContain("(none)"); // For missing description/context
|
|
549
|
-
expect(result).toContain("(no files assigned)"); // Empty files
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
describe("swarm_evaluation_prompt", () => {
|
|
554
|
-
it("generates evaluation prompt with schema hint", async () => {
|
|
555
|
-
const result = await swarm_evaluation_prompt.execute(
|
|
556
|
-
{
|
|
557
|
-
bead_id: "bd-abc123.1",
|
|
558
|
-
subtask_title: "Add OAuth provider",
|
|
559
|
-
files_touched: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
560
|
-
},
|
|
561
|
-
mockContext,
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
const parsed = JSON.parse(result);
|
|
565
|
-
|
|
566
|
-
expect(parsed).toHaveProperty("prompt");
|
|
567
|
-
expect(parsed).toHaveProperty("expected_schema", "Evaluation");
|
|
568
|
-
expect(parsed).toHaveProperty("schema_hint");
|
|
569
|
-
|
|
570
|
-
expect(parsed.prompt).toContain("bd-abc123.1");
|
|
571
|
-
expect(parsed.prompt).toContain("Add OAuth provider");
|
|
572
|
-
expect(parsed.prompt).toContain("src/auth/google.ts");
|
|
573
|
-
expect(parsed.prompt).toContain("type_safe");
|
|
574
|
-
expect(parsed.prompt).toContain("no_bugs");
|
|
575
|
-
expect(parsed.prompt).toContain("patterns");
|
|
576
|
-
expect(parsed.prompt).toContain("readable");
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("handles empty files list", async () => {
|
|
580
|
-
const result = await swarm_evaluation_prompt.execute(
|
|
581
|
-
{
|
|
582
|
-
bead_id: "bd-xyz789.1",
|
|
583
|
-
subtask_title: "Documentation only",
|
|
584
|
-
files_touched: [],
|
|
585
|
-
},
|
|
586
|
-
mockContext,
|
|
587
|
-
);
|
|
588
|
-
|
|
589
|
-
const parsed = JSON.parse(result);
|
|
590
|
-
|
|
591
|
-
expect(parsed.prompt).toContain("(no files recorded)");
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// ============================================================================
|
|
596
|
-
// Integration Tests (Require Agent Mail + beads)
|
|
597
|
-
// ============================================================================
|
|
598
|
-
|
|
599
|
-
describe("swarm_status (integration)", () => {
|
|
600
|
-
let beadsAvailable = false;
|
|
601
|
-
|
|
602
|
-
beforeAll(async () => {
|
|
603
|
-
beadsAvailable = await isBeadsAvailable();
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
it.skipIf(!beadsAvailable)(
|
|
607
|
-
"returns status for non-existent epic",
|
|
608
|
-
async () => {
|
|
609
|
-
// This should fail gracefully - no epic exists
|
|
610
|
-
try {
|
|
611
|
-
await swarm_status.execute(
|
|
612
|
-
{
|
|
613
|
-
epic_id: "bd-nonexistent",
|
|
614
|
-
project_key: TEST_PROJECT_PATH,
|
|
615
|
-
},
|
|
616
|
-
mockContext,
|
|
617
|
-
);
|
|
618
|
-
// If it doesn't throw, that's fine too - it might return empty status
|
|
619
|
-
} catch (error) {
|
|
620
|
-
expect(error).toBeInstanceOf(Error);
|
|
621
|
-
// SwarmError should have operation property
|
|
622
|
-
if (error instanceof Error && "operation" in error) {
|
|
623
|
-
expect((error as { operation: string }).operation).toBe(
|
|
624
|
-
"query_subtasks",
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
},
|
|
629
|
-
);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
describe("swarm_progress (integration)", () => {
|
|
633
|
-
let agentMailAvailable = false;
|
|
634
|
-
|
|
635
|
-
beforeAll(async () => {
|
|
636
|
-
agentMailAvailable = await isAgentMailAvailable();
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it.skipIf(!agentMailAvailable)("reports progress to Agent Mail", async () => {
|
|
640
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-progress-${Date.now()}`;
|
|
641
|
-
const sessionID = `progress-session-${Date.now()}`;
|
|
642
|
-
|
|
643
|
-
// Initialize Agent Mail state for this session
|
|
644
|
-
try {
|
|
645
|
-
// Ensure project exists
|
|
646
|
-
await mcpCall("ensure_project", { human_key: uniqueProjectKey });
|
|
647
|
-
|
|
648
|
-
// Register agent
|
|
649
|
-
const agent = await mcpCall<{ name: string }>("register_agent", {
|
|
650
|
-
project_key: uniqueProjectKey,
|
|
651
|
-
program: "opencode-test",
|
|
652
|
-
model: "test",
|
|
653
|
-
task_description: "Integration test",
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
// Set state for the session
|
|
657
|
-
setState(sessionID, {
|
|
658
|
-
projectKey: uniqueProjectKey,
|
|
659
|
-
agentName: agent.name,
|
|
660
|
-
reservations: [],
|
|
661
|
-
startedAt: new Date().toISOString(),
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
const ctx = {
|
|
665
|
-
...mockContext,
|
|
666
|
-
sessionID,
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
const result = await swarm_progress.execute(
|
|
670
|
-
{
|
|
671
|
-
project_key: uniqueProjectKey,
|
|
672
|
-
agent_name: agent.name,
|
|
673
|
-
bead_id: "bd-test123.1",
|
|
674
|
-
status: "in_progress",
|
|
675
|
-
message: "Working on the feature",
|
|
676
|
-
progress_percent: 50,
|
|
677
|
-
files_touched: ["src/test.ts"],
|
|
678
|
-
},
|
|
679
|
-
ctx,
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
expect(result).toContain("Progress reported");
|
|
683
|
-
expect(result).toContain("in_progress");
|
|
684
|
-
expect(result).toContain("50%");
|
|
685
|
-
} finally {
|
|
686
|
-
clearState(sessionID);
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
describe("swarm_complete (integration)", () => {
|
|
692
|
-
let agentMailAvailable = false;
|
|
693
|
-
let beadsAvailable = false;
|
|
694
|
-
|
|
695
|
-
beforeAll(async () => {
|
|
696
|
-
agentMailAvailable = await isAgentMailAvailable();
|
|
697
|
-
beadsAvailable = await isBeadsAvailable();
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
it.skipIf(!agentMailAvailable || !beadsAvailable)(
|
|
701
|
-
"completes subtask with passing evaluation",
|
|
702
|
-
async () => {
|
|
703
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-complete-${Date.now()}`;
|
|
704
|
-
const sessionID = `complete-session-${Date.now()}`;
|
|
705
|
-
|
|
706
|
-
try {
|
|
707
|
-
// Set up Agent Mail
|
|
708
|
-
await mcpCall("ensure_project", { human_key: uniqueProjectKey });
|
|
709
|
-
const agent = await mcpCall<{ name: string }>("register_agent", {
|
|
710
|
-
project_key: uniqueProjectKey,
|
|
711
|
-
program: "opencode-test",
|
|
712
|
-
model: "test",
|
|
713
|
-
task_description: "Integration test",
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
setState(sessionID, {
|
|
717
|
-
projectKey: uniqueProjectKey,
|
|
718
|
-
agentName: agent.name,
|
|
719
|
-
reservations: [],
|
|
720
|
-
startedAt: new Date().toISOString(),
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
const ctx = {
|
|
724
|
-
...mockContext,
|
|
725
|
-
sessionID,
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
// Create a test bead first
|
|
729
|
-
const createResult =
|
|
730
|
-
await Bun.$`bd create "Test subtask" -t task --json`
|
|
731
|
-
.quiet()
|
|
732
|
-
.nothrow();
|
|
733
|
-
|
|
734
|
-
if (createResult.exitCode !== 0) {
|
|
735
|
-
console.warn(
|
|
736
|
-
"Could not create test bead:",
|
|
737
|
-
createResult.stderr.toString(),
|
|
738
|
-
);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const bead = JSON.parse(createResult.stdout.toString());
|
|
743
|
-
|
|
744
|
-
const passingEvaluation = JSON.stringify({
|
|
745
|
-
passed: true,
|
|
746
|
-
criteria: {
|
|
747
|
-
type_safe: { passed: true, feedback: "All types correct" },
|
|
748
|
-
no_bugs: { passed: true, feedback: "No issues found" },
|
|
749
|
-
patterns: { passed: true, feedback: "Follows conventions" },
|
|
750
|
-
readable: { passed: true, feedback: "Clear code" },
|
|
751
|
-
},
|
|
752
|
-
overall_feedback: "Great work!",
|
|
753
|
-
retry_suggestion: null,
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
const result = await swarm_complete.execute(
|
|
757
|
-
{
|
|
758
|
-
project_key: uniqueProjectKey,
|
|
759
|
-
agent_name: agent.name,
|
|
760
|
-
bead_id: bead.id,
|
|
761
|
-
summary: "Completed the test subtask",
|
|
762
|
-
evaluation: passingEvaluation,
|
|
763
|
-
},
|
|
764
|
-
ctx,
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
const parsed = JSON.parse(result);
|
|
768
|
-
|
|
769
|
-
expect(parsed.success).toBe(true);
|
|
770
|
-
expect(parsed.bead_id).toBe(bead.id);
|
|
771
|
-
expect(parsed.closed).toBe(true);
|
|
772
|
-
expect(parsed.reservations_released).toBe(true);
|
|
773
|
-
expect(parsed.message_sent).toBe(true);
|
|
774
|
-
} finally {
|
|
775
|
-
clearState(sessionID);
|
|
776
|
-
}
|
|
777
|
-
},
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
it.skipIf(!agentMailAvailable)(
|
|
781
|
-
"rejects completion with failing evaluation",
|
|
782
|
-
async () => {
|
|
783
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-fail-${Date.now()}`;
|
|
784
|
-
const sessionID = `fail-session-${Date.now()}`;
|
|
785
|
-
|
|
786
|
-
try {
|
|
787
|
-
// Set up Agent Mail
|
|
788
|
-
await mcpCall("ensure_project", { human_key: uniqueProjectKey });
|
|
789
|
-
const agent = await mcpCall<{ name: string }>("register_agent", {
|
|
790
|
-
project_key: uniqueProjectKey,
|
|
791
|
-
program: "opencode-test",
|
|
792
|
-
model: "test",
|
|
793
|
-
task_description: "Integration test",
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
setState(sessionID, {
|
|
797
|
-
projectKey: uniqueProjectKey,
|
|
798
|
-
agentName: agent.name,
|
|
799
|
-
reservations: [],
|
|
800
|
-
startedAt: new Date().toISOString(),
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
const ctx = {
|
|
804
|
-
...mockContext,
|
|
805
|
-
sessionID,
|
|
806
|
-
};
|
|
807
|
-
|
|
808
|
-
const failingEvaluation = JSON.stringify({
|
|
809
|
-
passed: false,
|
|
810
|
-
criteria: {
|
|
811
|
-
type_safe: { passed: false, feedback: "Missing types on line 42" },
|
|
812
|
-
},
|
|
813
|
-
overall_feedback: "Needs work",
|
|
814
|
-
retry_suggestion: "Add explicit types to the handler function",
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
const result = await swarm_complete.execute(
|
|
818
|
-
{
|
|
819
|
-
project_key: uniqueProjectKey,
|
|
820
|
-
agent_name: agent.name,
|
|
821
|
-
bead_id: "bd-test-fail.1",
|
|
822
|
-
summary: "Attempted completion",
|
|
823
|
-
evaluation: failingEvaluation,
|
|
824
|
-
},
|
|
825
|
-
ctx,
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
const parsed = JSON.parse(result);
|
|
829
|
-
|
|
830
|
-
expect(parsed.success).toBe(false);
|
|
831
|
-
expect(parsed.error).toContain("Self-evaluation failed");
|
|
832
|
-
expect(parsed.retry_suggestion).toBe(
|
|
833
|
-
"Add explicit types to the handler function",
|
|
834
|
-
);
|
|
835
|
-
} finally {
|
|
836
|
-
clearState(sessionID);
|
|
837
|
-
}
|
|
838
|
-
},
|
|
839
|
-
);
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
// ============================================================================
|
|
843
|
-
// Full Swarm Flow (End-to-End)
|
|
844
|
-
// ============================================================================
|
|
845
|
-
|
|
846
|
-
describe("full swarm flow (integration)", () => {
|
|
847
|
-
let agentMailAvailable = false;
|
|
848
|
-
let beadsAvailable = false;
|
|
849
|
-
|
|
850
|
-
beforeAll(async () => {
|
|
851
|
-
agentMailAvailable = await isAgentMailAvailable();
|
|
852
|
-
beadsAvailable = await isBeadsAvailable();
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
it.skipIf(!agentMailAvailable || !beadsAvailable)(
|
|
856
|
-
"creates epic, reports progress, completes subtask",
|
|
857
|
-
async () => {
|
|
858
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-flow-${Date.now()}`;
|
|
859
|
-
const sessionID = `flow-session-${Date.now()}`;
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
// 1. Set up Agent Mail session
|
|
863
|
-
await mcpCall("ensure_project", { human_key: uniqueProjectKey });
|
|
864
|
-
const agent = await mcpCall<{ name: string }>("register_agent", {
|
|
865
|
-
project_key: uniqueProjectKey,
|
|
866
|
-
program: "opencode-test",
|
|
867
|
-
model: "test",
|
|
868
|
-
task_description: "E2E swarm test",
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
setState(sessionID, {
|
|
872
|
-
projectKey: uniqueProjectKey,
|
|
873
|
-
agentName: agent.name,
|
|
874
|
-
reservations: [],
|
|
875
|
-
startedAt: new Date().toISOString(),
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
const ctx = {
|
|
879
|
-
...mockContext,
|
|
880
|
-
sessionID,
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
// 2. Generate decomposition prompt
|
|
884
|
-
const decomposeResult = await swarm_decompose.execute(
|
|
885
|
-
{
|
|
886
|
-
task: "Add unit tests for auth module",
|
|
887
|
-
},
|
|
888
|
-
ctx,
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
const decomposition = JSON.parse(decomposeResult);
|
|
892
|
-
expect(decomposition.prompt).toContain("Add unit tests");
|
|
893
|
-
|
|
894
|
-
// 3. Create an epic with bd CLI
|
|
895
|
-
const epicResult =
|
|
896
|
-
await Bun.$`bd create "Add unit tests for auth module" -t epic --json`
|
|
897
|
-
.quiet()
|
|
898
|
-
.nothrow();
|
|
899
|
-
|
|
900
|
-
if (epicResult.exitCode !== 0) {
|
|
901
|
-
console.warn("Could not create epic:", epicResult.stderr.toString());
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const epic = JSON.parse(epicResult.stdout.toString());
|
|
906
|
-
expect(epic.id).toMatch(/^[a-z0-9-]+-[a-z0-9]+$/);
|
|
907
|
-
|
|
908
|
-
// 4. Create a subtask
|
|
909
|
-
const subtaskResult =
|
|
910
|
-
await Bun.$`bd create "Test login flow" -t task --json`
|
|
911
|
-
.quiet()
|
|
912
|
-
.nothrow();
|
|
913
|
-
|
|
914
|
-
if (subtaskResult.exitCode !== 0) {
|
|
915
|
-
console.warn(
|
|
916
|
-
"Could not create subtask:",
|
|
917
|
-
subtaskResult.stderr.toString(),
|
|
918
|
-
);
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const subtask = JSON.parse(subtaskResult.stdout.toString());
|
|
923
|
-
|
|
924
|
-
// 5. Generate subtask prompt
|
|
925
|
-
const subtaskPrompt = await swarm_subtask_prompt.execute(
|
|
926
|
-
{
|
|
927
|
-
agent_name: agent.name,
|
|
928
|
-
bead_id: subtask.id,
|
|
929
|
-
epic_id: epic.id,
|
|
930
|
-
subtask_title: "Test login flow",
|
|
931
|
-
files: ["src/auth/__tests__/login.test.ts"],
|
|
932
|
-
},
|
|
933
|
-
ctx,
|
|
934
|
-
);
|
|
935
|
-
|
|
936
|
-
expect(subtaskPrompt).toContain(agent.name);
|
|
937
|
-
expect(subtaskPrompt).toContain(subtask.id);
|
|
938
|
-
|
|
939
|
-
// 6. Report progress
|
|
940
|
-
const progressResult = await swarm_progress.execute(
|
|
941
|
-
{
|
|
942
|
-
project_key: uniqueProjectKey,
|
|
943
|
-
agent_name: agent.name,
|
|
944
|
-
bead_id: subtask.id,
|
|
945
|
-
status: "in_progress",
|
|
946
|
-
progress_percent: 50,
|
|
947
|
-
message: "Writing test cases",
|
|
948
|
-
},
|
|
949
|
-
ctx,
|
|
950
|
-
);
|
|
951
|
-
|
|
952
|
-
expect(progressResult).toContain("Progress reported");
|
|
953
|
-
|
|
954
|
-
// 7. Generate evaluation prompt
|
|
955
|
-
const evalPromptResult = await swarm_evaluation_prompt.execute(
|
|
956
|
-
{
|
|
957
|
-
bead_id: subtask.id,
|
|
958
|
-
subtask_title: "Test login flow",
|
|
959
|
-
files_touched: ["src/auth/__tests__/login.test.ts"],
|
|
960
|
-
},
|
|
961
|
-
ctx,
|
|
962
|
-
);
|
|
963
|
-
|
|
964
|
-
const evalPrompt = JSON.parse(evalPromptResult);
|
|
965
|
-
expect(evalPrompt.expected_schema).toBe("Evaluation");
|
|
966
|
-
|
|
967
|
-
// 8. Complete the subtask
|
|
968
|
-
const completeResult = await swarm_complete.execute(
|
|
969
|
-
{
|
|
970
|
-
project_key: uniqueProjectKey,
|
|
971
|
-
agent_name: agent.name,
|
|
972
|
-
bead_id: subtask.id,
|
|
973
|
-
summary: "Added comprehensive login tests",
|
|
974
|
-
evaluation: JSON.stringify({
|
|
975
|
-
passed: true,
|
|
976
|
-
criteria: {
|
|
977
|
-
type_safe: { passed: true, feedback: "TypeScript compiles" },
|
|
978
|
-
no_bugs: { passed: true, feedback: "Tests pass" },
|
|
979
|
-
patterns: { passed: true, feedback: "Follows test patterns" },
|
|
980
|
-
readable: { passed: true, feedback: "Clear test names" },
|
|
981
|
-
},
|
|
982
|
-
overall_feedback: "Good test coverage",
|
|
983
|
-
retry_suggestion: null,
|
|
984
|
-
}),
|
|
985
|
-
},
|
|
986
|
-
ctx,
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
const completion = JSON.parse(completeResult);
|
|
990
|
-
expect(completion.success).toBe(true);
|
|
991
|
-
expect(completion.closed).toBe(true);
|
|
992
|
-
expect(completion.message_sent).toBe(true);
|
|
993
|
-
|
|
994
|
-
// 9. Check swarm status
|
|
995
|
-
const statusResult = await swarm_status.execute(
|
|
996
|
-
{
|
|
997
|
-
epic_id: epic.id,
|
|
998
|
-
project_key: uniqueProjectKey,
|
|
999
|
-
},
|
|
1000
|
-
ctx,
|
|
1001
|
-
);
|
|
1002
|
-
|
|
1003
|
-
const status = JSON.parse(statusResult);
|
|
1004
|
-
expect(status.epic_id).toBe(epic.id);
|
|
1005
|
-
// Status may show completed subtasks now
|
|
1006
|
-
} finally {
|
|
1007
|
-
clearState(sessionID);
|
|
1008
|
-
}
|
|
1009
|
-
},
|
|
1010
|
-
);
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
// ============================================================================
|
|
1014
|
-
// Tool Availability & Graceful Degradation Tests
|
|
1015
|
-
// ============================================================================
|
|
1016
|
-
|
|
1017
|
-
import {
|
|
1018
|
-
checkTool,
|
|
1019
|
-
isToolAvailable,
|
|
1020
|
-
checkAllTools,
|
|
1021
|
-
formatToolAvailability,
|
|
1022
|
-
resetToolCache,
|
|
1023
|
-
withToolFallback,
|
|
1024
|
-
ifToolAvailable,
|
|
1025
|
-
} from "./tool-availability";
|
|
1026
|
-
import { swarm_init } from "./swarm";
|
|
1027
|
-
|
|
1028
|
-
describe("Tool Availability", () => {
|
|
1029
|
-
beforeAll(() => {
|
|
1030
|
-
resetToolCache();
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
afterAll(() => {
|
|
1034
|
-
resetToolCache();
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
it("checks individual tool availability", async () => {
|
|
1038
|
-
const status = await checkTool("semantic-memory");
|
|
1039
|
-
expect(status).toHaveProperty("available");
|
|
1040
|
-
expect(status).toHaveProperty("checkedAt");
|
|
1041
|
-
expect(typeof status.available).toBe("boolean");
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
it("caches tool availability checks", async () => {
|
|
1045
|
-
const status1 = await checkTool("semantic-memory");
|
|
1046
|
-
const status2 = await checkTool("semantic-memory");
|
|
1047
|
-
// Same timestamp means cached
|
|
1048
|
-
expect(status1.checkedAt).toBe(status2.checkedAt);
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
it("checks all tools at once", async () => {
|
|
1052
|
-
const availability = await checkAllTools();
|
|
1053
|
-
expect(availability.size).toBe(7); // semantic-memory, cass, ubs, hive, beads, swarm-mail, agent-mail
|
|
1054
|
-
expect(availability.has("semantic-memory")).toBe(true);
|
|
1055
|
-
expect(availability.has("cass")).toBe(true);
|
|
1056
|
-
expect(availability.has("ubs")).toBe(true);
|
|
1057
|
-
expect(availability.has("beads")).toBe(true);
|
|
1058
|
-
expect(availability.has("swarm-mail")).toBe(true);
|
|
1059
|
-
expect(availability.has("agent-mail")).toBe(true);
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
it("formats tool availability for display", async () => {
|
|
1063
|
-
const availability = await checkAllTools();
|
|
1064
|
-
const formatted = formatToolAvailability(availability);
|
|
1065
|
-
expect(formatted).toContain("Tool Availability:");
|
|
1066
|
-
expect(formatted).toContain("semantic-memory");
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
it("executes with fallback when tool unavailable", async () => {
|
|
1070
|
-
// Force cache reset to test fresh
|
|
1071
|
-
resetToolCache();
|
|
1072
|
-
|
|
1073
|
-
const result = await withToolFallback(
|
|
1074
|
-
"ubs", // May or may not be available
|
|
1075
|
-
async () => "action-result",
|
|
1076
|
-
() => "fallback-result",
|
|
1077
|
-
);
|
|
1078
|
-
|
|
1079
|
-
// Either result is valid depending on tool availability
|
|
1080
|
-
expect(["action-result", "fallback-result"]).toContain(result);
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
it("returns undefined when tool unavailable with ifToolAvailable", async () => {
|
|
1084
|
-
resetToolCache();
|
|
1085
|
-
|
|
1086
|
-
// This will return undefined if agent-mail is not running
|
|
1087
|
-
const result = await ifToolAvailable("agent-mail", async () => "success");
|
|
1088
|
-
|
|
1089
|
-
// Result is either "success" or undefined
|
|
1090
|
-
expect([undefined, "success"]).toContain(result);
|
|
1091
|
-
});
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
describe("swarm_init", () => {
|
|
1095
|
-
it("reports tool availability status", async () => {
|
|
1096
|
-
resetToolCache();
|
|
1097
|
-
|
|
1098
|
-
const result = await swarm_init.execute({}, mockContext);
|
|
1099
|
-
const parsed = JSON.parse(result);
|
|
1100
|
-
|
|
1101
|
-
expect(parsed).toHaveProperty("ready", true);
|
|
1102
|
-
expect(parsed).toHaveProperty("tool_availability");
|
|
1103
|
-
expect(parsed).toHaveProperty("report");
|
|
1104
|
-
|
|
1105
|
-
// Check tool availability structure
|
|
1106
|
-
const tools = parsed.tool_availability;
|
|
1107
|
-
expect(tools).toHaveProperty("semantic-memory");
|
|
1108
|
-
expect(tools).toHaveProperty("cass");
|
|
1109
|
-
expect(tools).toHaveProperty("ubs");
|
|
1110
|
-
expect(tools).toHaveProperty("beads");
|
|
1111
|
-
expect(tools).toHaveProperty("agent-mail");
|
|
1112
|
-
|
|
1113
|
-
// Each tool should have available and fallback
|
|
1114
|
-
for (const [, info] of Object.entries(tools)) {
|
|
1115
|
-
expect(info).toHaveProperty("available");
|
|
1116
|
-
expect(info).toHaveProperty("fallback");
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
it("includes recommendations", async () => {
|
|
1121
|
-
const result = await swarm_init.execute({}, mockContext);
|
|
1122
|
-
const parsed = JSON.parse(result);
|
|
1123
|
-
|
|
1124
|
-
expect(parsed).toHaveProperty("recommendations");
|
|
1125
|
-
expect(parsed.recommendations).toHaveProperty("beads");
|
|
1126
|
-
expect(parsed.recommendations).toHaveProperty("agent_mail");
|
|
1127
|
-
});
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
describe("Worker Handoff Generation", () => {
|
|
1131
|
-
it("generateWorkerHandoff creates valid WorkerHandoff object", () => {
|
|
1132
|
-
// This will test the new function once we implement it
|
|
1133
|
-
const { generateWorkerHandoff } = require("./swarm-orchestrate");
|
|
1134
|
-
|
|
1135
|
-
const handoff = generateWorkerHandoff({
|
|
1136
|
-
task_id: "opencode-swarm-monorepo-lf2p4u-abc123.1",
|
|
1137
|
-
files_owned: ["src/auth.ts", "src/middleware.ts"],
|
|
1138
|
-
epic_summary: "Add OAuth authentication",
|
|
1139
|
-
your_role: "Implement OAuth provider",
|
|
1140
|
-
dependencies_completed: ["Database schema ready"],
|
|
1141
|
-
what_comes_next: "Integration tests",
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
// Verify contract section
|
|
1145
|
-
expect(handoff.contract.task_id).toBe("opencode-swarm-monorepo-lf2p4u-abc123.1");
|
|
1146
|
-
expect(handoff.contract.files_owned).toEqual(["src/auth.ts", "src/middleware.ts"]);
|
|
1147
|
-
expect(handoff.contract.files_readonly).toEqual([]);
|
|
1148
|
-
expect(handoff.contract.dependencies_completed).toEqual(["Database schema ready"]);
|
|
1149
|
-
expect(handoff.contract.success_criteria.length).toBeGreaterThan(0);
|
|
1150
|
-
|
|
1151
|
-
// Verify context section
|
|
1152
|
-
expect(handoff.context.epic_summary).toBe("Add OAuth authentication");
|
|
1153
|
-
expect(handoff.context.your_role).toBe("Implement OAuth provider");
|
|
1154
|
-
expect(handoff.context.what_comes_next).toBe("Integration tests");
|
|
1155
|
-
|
|
1156
|
-
// Verify escalation section
|
|
1157
|
-
expect(handoff.escalation.blocked_contact).toBe("coordinator");
|
|
1158
|
-
expect(handoff.escalation.scope_change_protocol).toContain("swarmmail_send");
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
it("swarm_spawn_subtask includes handoff JSON in prompt", async () => {
|
|
1162
|
-
const result = await swarm_spawn_subtask.execute(
|
|
1163
|
-
{
|
|
1164
|
-
bead_id: "opencode-swarm-monorepo-lf2p4u-abc123.1",
|
|
1165
|
-
epic_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
1166
|
-
subtask_title: "Add OAuth provider",
|
|
1167
|
-
subtask_description: "Configure Google OAuth",
|
|
1168
|
-
files: ["src/auth/google.ts"],
|
|
1169
|
-
shared_context: "Using NextAuth.js v5",
|
|
1170
|
-
},
|
|
1171
|
-
mockContext,
|
|
1172
|
-
);
|
|
1173
|
-
|
|
1174
|
-
// Parse the JSON response
|
|
1175
|
-
const parsed = JSON.parse(result);
|
|
1176
|
-
const prompt = parsed.prompt;
|
|
1177
|
-
|
|
1178
|
-
// Should contain WorkerHandoff JSON section
|
|
1179
|
-
expect(prompt).toContain("## WorkerHandoff Contract");
|
|
1180
|
-
expect(prompt).toContain('"contract"');
|
|
1181
|
-
expect(prompt).toContain('"task_id"');
|
|
1182
|
-
expect(prompt).toContain('"files_owned"');
|
|
1183
|
-
expect(prompt).toContain('"success_criteria"');
|
|
1184
|
-
expect(prompt).toContain("opencode-swarm-monorepo-lf2p4u-abc123.1");
|
|
1185
|
-
});
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
describe("Graceful Degradation", () => {
|
|
1189
|
-
it("swarm_decompose works without CASS", async () => {
|
|
1190
|
-
// This should work regardless of CASS availability
|
|
1191
|
-
const result = await swarm_decompose.execute(
|
|
1192
|
-
{
|
|
1193
|
-
task: "Add user authentication",
|
|
1194
|
-
query_cass: true, // Request CASS but it may not be available
|
|
1195
|
-
},
|
|
1196
|
-
mockContext,
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
const parsed = JSON.parse(result);
|
|
1200
|
-
|
|
1201
|
-
// Should always return a valid prompt
|
|
1202
|
-
expect(parsed).toHaveProperty("prompt");
|
|
1203
|
-
expect(parsed.prompt).toContain("Add user authentication");
|
|
1204
|
-
|
|
1205
|
-
// CASS history should indicate whether it was queried
|
|
1206
|
-
expect(parsed).toHaveProperty("cass_history");
|
|
1207
|
-
expect(parsed.cass_history).toHaveProperty("queried");
|
|
1208
|
-
});
|
|
1209
|
-
|
|
1210
|
-
it("swarm_decompose can skip CASS explicitly", async () => {
|
|
1211
|
-
const result = await swarm_decompose.execute(
|
|
1212
|
-
{
|
|
1213
|
-
task: "Add user authentication",
|
|
1214
|
-
query_cass: false, // Explicitly skip CASS
|
|
1215
|
-
},
|
|
1216
|
-
mockContext,
|
|
1217
|
-
);
|
|
1218
|
-
|
|
1219
|
-
const parsed = JSON.parse(result);
|
|
1220
|
-
|
|
1221
|
-
expect(parsed.cass_history.queried).toBe(false);
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
it("decomposition prompt includes beads discipline", async () => {
|
|
1225
|
-
const result = await swarm_decompose.execute(
|
|
1226
|
-
{
|
|
1227
|
-
task: "Build feature X",
|
|
1228
|
-
},
|
|
1229
|
-
mockContext,
|
|
1230
|
-
);
|
|
1231
|
-
|
|
1232
|
-
const parsed = JSON.parse(result);
|
|
1233
|
-
|
|
1234
|
-
// Check that beads discipline is in the prompt
|
|
1235
|
-
expect(parsed.prompt).toContain("MANDATORY");
|
|
1236
|
-
expect(parsed.prompt).toContain("bead");
|
|
1237
|
-
expect(parsed.prompt).toContain("Plan aggressively");
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
it("subtask prompt includes agent-mail discipline", async () => {
|
|
1241
|
-
const result = await swarm_subtask_prompt.execute(
|
|
1242
|
-
{
|
|
1243
|
-
agent_name: "TestAgent",
|
|
1244
|
-
bead_id: "bd-test123.1",
|
|
1245
|
-
epic_id: "bd-test123",
|
|
1246
|
-
subtask_title: "Test task",
|
|
1247
|
-
files: ["src/test.ts"],
|
|
1248
|
-
},
|
|
1249
|
-
mockContext,
|
|
1250
|
-
);
|
|
1251
|
-
|
|
1252
|
-
// Check that swarm-mail discipline is in the prompt
|
|
1253
|
-
expect(result).toContain("MANDATORY");
|
|
1254
|
-
expect(result).toContain("Swarm Mail");
|
|
1255
|
-
expect(result).toContain("swarmmail_send");
|
|
1256
|
-
expect(result).toContain("Report progress");
|
|
1257
|
-
});
|
|
1258
|
-
});
|
|
1259
|
-
|
|
1260
|
-
// ============================================================================
|
|
1261
|
-
// Coordinator-Centric Swarm Tools (V2)
|
|
1262
|
-
// ============================================================================
|
|
1263
|
-
|
|
1264
|
-
describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
|
|
1265
|
-
describe("formatSubtaskPromptV2", () => {
|
|
1266
|
-
it("generates correct prompt with all fields", () => {
|
|
1267
|
-
const result = formatSubtaskPromptV2({
|
|
1268
|
-
bead_id: "test-swarm-plugin-lf2p4u-oauth123.1",
|
|
1269
|
-
epic_id: "test-swarm-plugin-lf2p4u-oauth123",
|
|
1270
|
-
subtask_title: "Add OAuth provider",
|
|
1271
|
-
subtask_description: "Configure Google OAuth in the auth config",
|
|
1272
|
-
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
1273
|
-
shared_context: "We are using NextAuth.js v5",
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
// Check title is included
|
|
1277
|
-
expect(result).toContain("Add OAuth provider");
|
|
1278
|
-
|
|
1279
|
-
// Check description is included
|
|
1280
|
-
expect(result).toContain("Configure Google OAuth in the auth config");
|
|
1281
|
-
|
|
1282
|
-
// Check files are formatted as list
|
|
1283
|
-
expect(result).toContain("- `src/auth/google.ts`");
|
|
1284
|
-
expect(result).toContain("- `src/auth/config.ts`");
|
|
1285
|
-
|
|
1286
|
-
// Check shared context is included
|
|
1287
|
-
expect(result).toContain("We are using NextAuth.js v5");
|
|
1288
|
-
|
|
1289
|
-
// Check bead/epic IDs are substituted
|
|
1290
|
-
expect(result).toContain("test-swarm-plugin-lf2p4u-oauth123.1");
|
|
1291
|
-
expect(result).toContain("test-swarm-plugin-lf2p4u-oauth123");
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
it("handles missing optional fields", () => {
|
|
1295
|
-
const result = formatSubtaskPromptV2({
|
|
1296
|
-
bead_id: "test-swarm-plugin-lf2p4u-simple456.1",
|
|
1297
|
-
epic_id: "test-swarm-plugin-lf2p4u-simple456",
|
|
1298
|
-
subtask_title: "Simple task",
|
|
1299
|
-
subtask_description: "",
|
|
1300
|
-
files: [],
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
// Check title is included
|
|
1304
|
-
expect(result).toContain("Simple task");
|
|
1305
|
-
|
|
1306
|
-
// Check fallback for empty description
|
|
1307
|
-
expect(result).toContain("(see title)");
|
|
1308
|
-
|
|
1309
|
-
// Check fallback for empty files
|
|
1310
|
-
expect(result).toContain("(no specific files - use judgment)");
|
|
1311
|
-
|
|
1312
|
-
// Check fallback for missing context
|
|
1313
|
-
expect(result).toContain("(none)");
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
it("handles files with special characters", () => {
|
|
1317
|
-
const result = formatSubtaskPromptV2({
|
|
1318
|
-
bead_id: "test-swarm-plugin-lf2p4u-paths789.1",
|
|
1319
|
-
epic_id: "test-swarm-plugin-lf2p4u-paths789",
|
|
1320
|
-
subtask_title: "Handle paths",
|
|
1321
|
-
subtask_description: "Test file paths",
|
|
1322
|
-
files: [
|
|
1323
|
-
"src/components/[slug]/page.tsx",
|
|
1324
|
-
"src/api/users/[id]/route.ts",
|
|
1325
|
-
],
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
expect(result).toContain("- `src/components/[slug]/page.tsx`");
|
|
1329
|
-
expect(result).toContain("- `src/api/users/[id]/route.ts`");
|
|
1330
|
-
});
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
describe("SUBTASK_PROMPT_V2", () => {
|
|
1334
|
-
it("contains expected sections", () => {
|
|
1335
|
-
// Check all main sections are present in the template
|
|
1336
|
-
expect(SUBTASK_PROMPT_V2).toContain("[TASK]");
|
|
1337
|
-
expect(SUBTASK_PROMPT_V2).toContain("{subtask_title}");
|
|
1338
|
-
expect(SUBTASK_PROMPT_V2).toContain("{subtask_description}");
|
|
1339
|
-
|
|
1340
|
-
expect(SUBTASK_PROMPT_V2).toContain("[FILES]");
|
|
1341
|
-
expect(SUBTASK_PROMPT_V2).toContain("{file_list}");
|
|
1342
|
-
|
|
1343
|
-
expect(SUBTASK_PROMPT_V2).toContain("[CONTEXT]");
|
|
1344
|
-
expect(SUBTASK_PROMPT_V2).toContain("{shared_context}");
|
|
1345
|
-
|
|
1346
|
-
expect(SUBTASK_PROMPT_V2).toContain("[MANDATORY SURVIVAL CHECKLIST]");
|
|
1347
|
-
});
|
|
1348
|
-
|
|
1349
|
-
it("DOES contain Swarm Mail instructions (MANDATORY)", () => {
|
|
1350
|
-
// V2 prompt tells agents to USE Swarm Mail - this is non-negotiable
|
|
1351
|
-
expect(SUBTASK_PROMPT_V2).toContain("SWARM MAIL");
|
|
1352
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_init");
|
|
1353
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_send");
|
|
1354
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_inbox");
|
|
1355
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_reserve");
|
|
1356
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_release");
|
|
1357
|
-
expect(SUBTASK_PROMPT_V2).toContain("thread_id");
|
|
1358
|
-
expect(SUBTASK_PROMPT_V2).toContain("NON-NEGOTIABLE");
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
it("DOES contain beads instructions", () => {
|
|
1362
|
-
// V2 prompt tells agents to USE beads
|
|
1363
|
-
expect(SUBTASK_PROMPT_V2).toContain("{bead_id}");
|
|
1364
|
-
expect(SUBTASK_PROMPT_V2).toContain("{epic_id}");
|
|
1365
|
-
expect(SUBTASK_PROMPT_V2).toContain("hive_update");
|
|
1366
|
-
expect(SUBTASK_PROMPT_V2).toContain("hive_create");
|
|
1367
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarm_complete");
|
|
1368
|
-
});
|
|
1369
|
-
|
|
1370
|
-
it("grants workers autonomy to file beads against epic", () => {
|
|
1371
|
-
// Workers should be able to file bugs, tech debt, follow-ups
|
|
1372
|
-
expect(SUBTASK_PROMPT_V2).toContain("You Have Autonomy to File Issues");
|
|
1373
|
-
expect(SUBTASK_PROMPT_V2).toContain("parent_id");
|
|
1374
|
-
expect(SUBTASK_PROMPT_V2).toContain("Don't silently ignore issues");
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
it("instructs agents to communicate via swarmmail", () => {
|
|
1378
|
-
expect(SUBTASK_PROMPT_V2).toContain("don't work silently");
|
|
1379
|
-
expect(SUBTASK_PROMPT_V2).toContain("progress");
|
|
1380
|
-
expect(SUBTASK_PROMPT_V2).toContain("coordinator");
|
|
1381
|
-
expect(SUBTASK_PROMPT_V2).toContain("CRITICAL");
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
it("contains survival checklist: semantic-memory_find", () => {
|
|
1385
|
-
// Step 2: Query past learnings BEFORE starting work
|
|
1386
|
-
expect(SUBTASK_PROMPT_V2).toContain("semantic-memory_find");
|
|
1387
|
-
expect(SUBTASK_PROMPT_V2).toContain("Query Past Learnings");
|
|
1388
|
-
expect(SUBTASK_PROMPT_V2).toContain("BEFORE starting work");
|
|
1389
|
-
expect(SUBTASK_PROMPT_V2).toContain("If you skip this step, you WILL waste time solving already-solved problems");
|
|
1390
|
-
});
|
|
1391
|
-
|
|
1392
|
-
it("contains survival checklist: skills discovery and loading", () => {
|
|
1393
|
-
// Step 3: Load relevant skills if available
|
|
1394
|
-
expect(SUBTASK_PROMPT_V2).toContain("skills_list");
|
|
1395
|
-
expect(SUBTASK_PROMPT_V2).toContain("skills_use");
|
|
1396
|
-
expect(SUBTASK_PROMPT_V2).toContain("Load Relevant Skills");
|
|
1397
|
-
expect(SUBTASK_PROMPT_V2).toContain("Common skill triggers");
|
|
1398
|
-
});
|
|
1399
|
-
|
|
1400
|
-
it("contains survival checklist: worker reserves files (not coordinator)", () => {
|
|
1401
|
-
// Step 4: Worker reserves their own files
|
|
1402
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_reserve");
|
|
1403
|
-
expect(SUBTASK_PROMPT_V2).toContain("Reserve Your Files");
|
|
1404
|
-
expect(SUBTASK_PROMPT_V2).toContain("YOU reserve, not coordinator");
|
|
1405
|
-
expect(SUBTASK_PROMPT_V2).toContain("Workers reserve their own files");
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
it("contains survival checklist: swarm_progress at milestones", () => {
|
|
1409
|
-
// Step 6: Report progress at 25/50/75%
|
|
1410
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarm_progress");
|
|
1411
|
-
expect(SUBTASK_PROMPT_V2).toContain("Report Progress at Milestones");
|
|
1412
|
-
expect(SUBTASK_PROMPT_V2).toContain("progress_percent");
|
|
1413
|
-
expect(SUBTASK_PROMPT_V2).toContain("25%, 50%, 75%");
|
|
1414
|
-
expect(SUBTASK_PROMPT_V2).toContain("auto-checkpoint");
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
it("contains survival checklist: swarm_checkpoint before risky ops", () => {
|
|
1418
|
-
// Step 7: Manual checkpoint before risky operations
|
|
1419
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarm_checkpoint");
|
|
1420
|
-
expect(SUBTASK_PROMPT_V2).toContain("Manual Checkpoint BEFORE Risky Operations");
|
|
1421
|
-
expect(SUBTASK_PROMPT_V2).toContain("Large refactors");
|
|
1422
|
-
expect(SUBTASK_PROMPT_V2).toContain("preserve context");
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
it("contains survival checklist: semantic-memory_store for learnings", () => {
|
|
1426
|
-
// Step 8: Store discoveries and learnings
|
|
1427
|
-
expect(SUBTASK_PROMPT_V2).toContain("semantic-memory_store");
|
|
1428
|
-
expect(SUBTASK_PROMPT_V2).toContain("STORE YOUR LEARNINGS");
|
|
1429
|
-
expect(SUBTASK_PROMPT_V2).toContain("Solved a tricky bug");
|
|
1430
|
-
expect(SUBTASK_PROMPT_V2).toContain("The WHY matters more than the WHAT");
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
it("does NOT mention coordinator reserving files", () => {
|
|
1434
|
-
// Coordinator no longer reserves files - workers do it themselves
|
|
1435
|
-
const lowerPrompt = SUBTASK_PROMPT_V2.toLowerCase();
|
|
1436
|
-
expect(lowerPrompt).not.toContain("coordinator reserves");
|
|
1437
|
-
expect(lowerPrompt).not.toContain("coordinator will reserve");
|
|
1438
|
-
});
|
|
1439
|
-
|
|
1440
|
-
it("enforces swarm_complete over manual hive_close", () => {
|
|
1441
|
-
// Step 9: Use swarm_complete, not hive_close
|
|
1442
|
-
expect(SUBTASK_PROMPT_V2).toContain("swarm_complete");
|
|
1443
|
-
expect(SUBTASK_PROMPT_V2).toContain("DO NOT manually close the cell");
|
|
1444
|
-
expect(SUBTASK_PROMPT_V2).toContain("Use swarm_complete");
|
|
1445
|
-
});
|
|
1446
|
-
});
|
|
1447
|
-
|
|
1448
|
-
describe("swarm_complete automatic memory capture", () => {
|
|
1449
|
-
let beadsAvailable = false;
|
|
1450
|
-
|
|
1451
|
-
beforeAll(async () => {
|
|
1452
|
-
beadsAvailable = await isBeadsAvailable();
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
it.skipIf(!beadsAvailable)(
|
|
1456
|
-
"includes memory_capture object in response",
|
|
1457
|
-
async () => {
|
|
1458
|
-
// Create a real bead for the test
|
|
1459
|
-
const createResult =
|
|
1460
|
-
await Bun.$`bd create "Test memory capture" -t task --json`
|
|
1461
|
-
.quiet()
|
|
1462
|
-
.nothrow();
|
|
1463
|
-
|
|
1464
|
-
if (createResult.exitCode !== 0) {
|
|
1465
|
-
console.warn(
|
|
1466
|
-
"Could not create bead:",
|
|
1467
|
-
createResult.stderr.toString(),
|
|
1468
|
-
);
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const bead = JSON.parse(createResult.stdout.toString());
|
|
1473
|
-
|
|
1474
|
-
try {
|
|
1475
|
-
const result = await swarm_complete.execute(
|
|
1476
|
-
{
|
|
1477
|
-
project_key: "/tmp/test-memory-capture",
|
|
1478
|
-
agent_name: "test-agent",
|
|
1479
|
-
bead_id: bead.id,
|
|
1480
|
-
summary: "Implemented auto-capture feature",
|
|
1481
|
-
files_touched: ["src/swarm-orchestrate.ts"],
|
|
1482
|
-
skip_verification: true,
|
|
1483
|
-
},
|
|
1484
|
-
mockContext,
|
|
1485
|
-
);
|
|
1486
|
-
|
|
1487
|
-
const parsed = JSON.parse(result);
|
|
1488
|
-
|
|
1489
|
-
// Verify memory capture was attempted
|
|
1490
|
-
expect(parsed).toHaveProperty("memory_capture");
|
|
1491
|
-
expect(parsed.memory_capture).toHaveProperty("attempted", true);
|
|
1492
|
-
expect(parsed.memory_capture).toHaveProperty("stored");
|
|
1493
|
-
expect(parsed.memory_capture).toHaveProperty("information");
|
|
1494
|
-
expect(parsed.memory_capture).toHaveProperty("metadata");
|
|
1495
|
-
|
|
1496
|
-
// Information should contain bead ID and summary
|
|
1497
|
-
expect(parsed.memory_capture.information).toContain(bead.id);
|
|
1498
|
-
expect(parsed.memory_capture.information).toContain(
|
|
1499
|
-
"Implemented auto-capture feature",
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
|
-
// Metadata should contain relevant tags
|
|
1503
|
-
expect(parsed.memory_capture.metadata).toContain("swarm");
|
|
1504
|
-
expect(parsed.memory_capture.metadata).toContain("success");
|
|
1505
|
-
} catch (error) {
|
|
1506
|
-
// Clean up bead if test fails
|
|
1507
|
-
await Bun.$`bd close ${bead.id} --reason "Test cleanup"`
|
|
1508
|
-
.quiet()
|
|
1509
|
-
.nothrow();
|
|
1510
|
-
throw error;
|
|
1511
|
-
}
|
|
1512
|
-
},
|
|
1513
|
-
);
|
|
1514
|
-
|
|
1515
|
-
it.skipIf(!beadsAvailable)(
|
|
1516
|
-
"attempts to store in semantic-memory when available",
|
|
1517
|
-
async () => {
|
|
1518
|
-
const createResult =
|
|
1519
|
-
await Bun.$`bd create "Test semantic-memory storage" -t task --json`
|
|
1520
|
-
.quiet()
|
|
1521
|
-
.nothrow();
|
|
1522
|
-
|
|
1523
|
-
if (createResult.exitCode !== 0) {
|
|
1524
|
-
console.warn(
|
|
1525
|
-
"Could not create bead:",
|
|
1526
|
-
createResult.stderr.toString(),
|
|
1527
|
-
);
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
const bead = JSON.parse(createResult.stdout.toString());
|
|
1532
|
-
|
|
1533
|
-
try {
|
|
1534
|
-
const result = await swarm_complete.execute(
|
|
1535
|
-
{
|
|
1536
|
-
project_key: "/tmp/test-memory-storage",
|
|
1537
|
-
agent_name: "test-agent",
|
|
1538
|
-
bead_id: bead.id,
|
|
1539
|
-
summary: "Fixed critical bug in auth flow",
|
|
1540
|
-
files_touched: ["src/auth.ts", "src/middleware.ts"],
|
|
1541
|
-
skip_verification: true,
|
|
1542
|
-
},
|
|
1543
|
-
mockContext,
|
|
1544
|
-
);
|
|
1545
|
-
|
|
1546
|
-
const parsed = JSON.parse(result);
|
|
1547
|
-
|
|
1548
|
-
// If semantic-memory is available, stored should be true
|
|
1549
|
-
// If not, error should explain why
|
|
1550
|
-
if (parsed.memory_capture.stored) {
|
|
1551
|
-
expect(parsed.memory_capture.note).toContain(
|
|
1552
|
-
"automatically stored in semantic-memory",
|
|
1553
|
-
);
|
|
1554
|
-
} else {
|
|
1555
|
-
expect(parsed.memory_capture.error).toBeDefined();
|
|
1556
|
-
expect(
|
|
1557
|
-
parsed.memory_capture.error.includes("not available") ||
|
|
1558
|
-
parsed.memory_capture.error.includes("failed"),
|
|
1559
|
-
).toBe(true);
|
|
1560
|
-
}
|
|
1561
|
-
} catch (error) {
|
|
1562
|
-
// Clean up bead if test fails
|
|
1563
|
-
await Bun.$`bd close ${bead.id} --reason "Test cleanup"`
|
|
1564
|
-
.quiet()
|
|
1565
|
-
.nothrow();
|
|
1566
|
-
throw error;
|
|
1567
|
-
}
|
|
1568
|
-
},
|
|
1569
|
-
);
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
|
-
describe("swarm_complete error handling", () => {
|
|
1573
|
-
let beadsAvailable = false;
|
|
1574
|
-
|
|
1575
|
-
beforeAll(async () => {
|
|
1576
|
-
beadsAvailable = await isBeadsAvailable();
|
|
1577
|
-
});
|
|
1578
|
-
|
|
1579
|
-
it.skipIf(!beadsAvailable)(
|
|
1580
|
-
"returns structured error when bead close fails",
|
|
1581
|
-
async () => {
|
|
1582
|
-
// Try to complete a non-existent bead
|
|
1583
|
-
const result = await swarm_complete.execute(
|
|
1584
|
-
{
|
|
1585
|
-
project_key: "/tmp/test-error-handling",
|
|
1586
|
-
agent_name: "test-agent",
|
|
1587
|
-
bead_id: "bd-nonexistent-12345",
|
|
1588
|
-
summary: "This should fail",
|
|
1589
|
-
skip_verification: true,
|
|
1590
|
-
},
|
|
1591
|
-
mockContext,
|
|
1592
|
-
);
|
|
1593
|
-
|
|
1594
|
-
const parsed = JSON.parse(result);
|
|
1595
|
-
|
|
1596
|
-
// Should return structured error, not throw
|
|
1597
|
-
expect(parsed.success).toBe(false);
|
|
1598
|
-
expect(parsed.error).toContain("Failed to close bead");
|
|
1599
|
-
expect(parsed.failed_step).toBe("bd close");
|
|
1600
|
-
expect(parsed.bead_id).toBe("bd-nonexistent-12345");
|
|
1601
|
-
expect(parsed.recovery).toBeDefined();
|
|
1602
|
-
expect(parsed.recovery.steps).toBeInstanceOf(Array);
|
|
1603
|
-
},
|
|
1604
|
-
);
|
|
1605
|
-
|
|
1606
|
-
it.skipIf(!beadsAvailable)(
|
|
1607
|
-
"returns specific error message when bead_id not found",
|
|
1608
|
-
async () => {
|
|
1609
|
-
// Try to complete with a non-existent bead ID
|
|
1610
|
-
const result = await swarm_complete.execute(
|
|
1611
|
-
{
|
|
1612
|
-
project_key: "/tmp/test-bead-not-found",
|
|
1613
|
-
agent_name: "test-agent",
|
|
1614
|
-
bead_id: "bd-totally-fake-xyz123",
|
|
1615
|
-
summary: "This should fail with specific error",
|
|
1616
|
-
skip_verification: true,
|
|
1617
|
-
},
|
|
1618
|
-
mockContext,
|
|
1619
|
-
);
|
|
1620
|
-
|
|
1621
|
-
const parsed = JSON.parse(result);
|
|
1622
|
-
|
|
1623
|
-
// Should return structured error with specific message
|
|
1624
|
-
expect(parsed.success).toBe(false);
|
|
1625
|
-
expect(parsed.error).toBeDefined();
|
|
1626
|
-
// RED: This will fail - we currently get generic "Tool execution failed"
|
|
1627
|
-
// We want the error message to specifically mention the bead was not found
|
|
1628
|
-
expect(
|
|
1629
|
-
parsed.error.toLowerCase().includes("bead not found") ||
|
|
1630
|
-
parsed.error.toLowerCase().includes("not found"),
|
|
1631
|
-
).toBe(true);
|
|
1632
|
-
expect(parsed.bead_id).toBe("bd-totally-fake-xyz123");
|
|
1633
|
-
},
|
|
1634
|
-
);
|
|
1635
|
-
|
|
1636
|
-
it.skipIf(!beadsAvailable)(
|
|
1637
|
-
"returns specific error when project_key is invalid/mismatched",
|
|
1638
|
-
async () => {
|
|
1639
|
-
// Create a real bead first
|
|
1640
|
-
const createResult =
|
|
1641
|
-
await Bun.$`bd create "Test project mismatch" -t task --json`
|
|
1642
|
-
.quiet()
|
|
1643
|
-
.nothrow();
|
|
1644
|
-
|
|
1645
|
-
if (createResult.exitCode !== 0) {
|
|
1646
|
-
console.warn(
|
|
1647
|
-
"Could not create bead:",
|
|
1648
|
-
createResult.stderr.toString(),
|
|
1649
|
-
);
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
const bead = JSON.parse(createResult.stdout.toString());
|
|
1654
|
-
|
|
1655
|
-
try {
|
|
1656
|
-
// Try to complete with mismatched project_key
|
|
1657
|
-
const result = await swarm_complete.execute(
|
|
1658
|
-
{
|
|
1659
|
-
project_key: "/totally/wrong/project/path",
|
|
1660
|
-
agent_name: "test-agent",
|
|
1661
|
-
bead_id: bead.id,
|
|
1662
|
-
summary: "This should fail with project mismatch",
|
|
1663
|
-
skip_verification: true,
|
|
1664
|
-
},
|
|
1665
|
-
mockContext,
|
|
1666
|
-
);
|
|
1667
|
-
|
|
1668
|
-
const parsed = JSON.parse(result);
|
|
1669
|
-
|
|
1670
|
-
// Should return structured error with specific message about project mismatch
|
|
1671
|
-
expect(parsed.success).toBe(false);
|
|
1672
|
-
expect(parsed.error).toBeDefined();
|
|
1673
|
-
// RED: This will fail - we want specific validation error
|
|
1674
|
-
// Error should mention project mismatch or validation failure
|
|
1675
|
-
const errorLower = parsed.error.toLowerCase();
|
|
1676
|
-
expect(
|
|
1677
|
-
(errorLower.includes("project") &&
|
|
1678
|
-
(errorLower.includes("mismatch") ||
|
|
1679
|
-
errorLower.includes("invalid") ||
|
|
1680
|
-
errorLower.includes("not found"))) ||
|
|
1681
|
-
errorLower.includes("validation"),
|
|
1682
|
-
).toBe(true);
|
|
1683
|
-
} finally {
|
|
1684
|
-
// Clean up
|
|
1685
|
-
await Bun.$`bd close ${bead.id} --reason "Test cleanup"`
|
|
1686
|
-
.quiet()
|
|
1687
|
-
.nothrow();
|
|
1688
|
-
}
|
|
1689
|
-
},
|
|
1690
|
-
);
|
|
1691
|
-
|
|
1692
|
-
it.skipIf(!beadsAvailable)(
|
|
1693
|
-
"includes message_sent status in response",
|
|
1694
|
-
async () => {
|
|
1695
|
-
const createResult =
|
|
1696
|
-
await Bun.$`bd create "Test message status" -t task --json`
|
|
1697
|
-
.quiet()
|
|
1698
|
-
.nothrow();
|
|
1699
|
-
|
|
1700
|
-
if (createResult.exitCode !== 0) {
|
|
1701
|
-
console.warn(
|
|
1702
|
-
"Could not create bead:",
|
|
1703
|
-
createResult.stderr.toString(),
|
|
1704
|
-
);
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
const bead = JSON.parse(createResult.stdout.toString());
|
|
1709
|
-
|
|
1710
|
-
try {
|
|
1711
|
-
const result = await swarm_complete.execute(
|
|
1712
|
-
{
|
|
1713
|
-
project_key: "/tmp/test-message-status",
|
|
1714
|
-
agent_name: "test-agent",
|
|
1715
|
-
bead_id: bead.id,
|
|
1716
|
-
summary: "Test message status tracking",
|
|
1717
|
-
skip_verification: true,
|
|
1718
|
-
},
|
|
1719
|
-
mockContext,
|
|
1720
|
-
);
|
|
1721
|
-
|
|
1722
|
-
const parsed = JSON.parse(result);
|
|
1723
|
-
|
|
1724
|
-
// Should have message_sent field (true or false)
|
|
1725
|
-
expect(parsed).toHaveProperty("message_sent");
|
|
1726
|
-
// If message failed, should have message_error
|
|
1727
|
-
if (!parsed.message_sent) {
|
|
1728
|
-
expect(parsed).toHaveProperty("message_error");
|
|
1729
|
-
}
|
|
1730
|
-
} catch (error) {
|
|
1731
|
-
// Clean up bead if test fails
|
|
1732
|
-
await Bun.$`bd close ${bead.id} --reason "Test cleanup"`
|
|
1733
|
-
.quiet()
|
|
1734
|
-
.nothrow();
|
|
1735
|
-
throw error;
|
|
1736
|
-
}
|
|
1737
|
-
},
|
|
1738
|
-
);
|
|
1739
|
-
});
|
|
1740
|
-
});
|
|
1741
|
-
|
|
1742
|
-
// ============================================================================
|
|
1743
|
-
// Checkpoint/Recovery Flow Integration Tests
|
|
1744
|
-
// ============================================================================
|
|
1745
|
-
|
|
1746
|
-
describe("Checkpoint/Recovery Flow (integration)", () => {
|
|
1747
|
-
describe("swarm_checkpoint", () => {
|
|
1748
|
-
it("creates swarm_checkpointed event and updates swarm_contexts table", async () => {
|
|
1749
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-checkpoint-${Date.now()}`;
|
|
1750
|
-
const sessionID = `checkpoint-session-${Date.now()}`;
|
|
1751
|
-
|
|
1752
|
-
// Initialize swarm-mail database directly (no Agent Mail needed)
|
|
1753
|
-
const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
|
|
1754
|
-
const swarmMail = await getSwarmMailLibSQL(uniqueProjectKey);
|
|
1755
|
-
const db = await swarmMail.getDatabase();
|
|
1756
|
-
|
|
1757
|
-
try {
|
|
1758
|
-
const ctx = {
|
|
1759
|
-
...mockContext,
|
|
1760
|
-
sessionID,
|
|
1761
|
-
};
|
|
1762
|
-
|
|
1763
|
-
const epicId = "bd-test-epic-123";
|
|
1764
|
-
const beadId = "bd-test-epic-123.1";
|
|
1765
|
-
const agentName = "TestAgent";
|
|
1766
|
-
|
|
1767
|
-
// Execute checkpoint
|
|
1768
|
-
const result = await swarm_checkpoint.execute(
|
|
1769
|
-
{
|
|
1770
|
-
project_key: uniqueProjectKey,
|
|
1771
|
-
agent_name: agentName,
|
|
1772
|
-
bead_id: beadId,
|
|
1773
|
-
epic_id: epicId,
|
|
1774
|
-
files_modified: ["src/test.ts", "src/test2.ts"],
|
|
1775
|
-
progress_percent: 50,
|
|
1776
|
-
directives: {
|
|
1777
|
-
shared_context: "Testing checkpoint functionality",
|
|
1778
|
-
skills_to_load: ["testing-patterns"],
|
|
1779
|
-
coordinator_notes: "Mid-task checkpoint",
|
|
1780
|
-
},
|
|
1781
|
-
},
|
|
1782
|
-
ctx,
|
|
1783
|
-
);
|
|
1784
|
-
|
|
1785
|
-
const parsed = JSON.parse(result);
|
|
1786
|
-
|
|
1787
|
-
// Verify checkpoint was created
|
|
1788
|
-
expect(parsed.success).toBe(true);
|
|
1789
|
-
expect(parsed.bead_id).toBe(beadId);
|
|
1790
|
-
expect(parsed.epic_id).toBe(epicId);
|
|
1791
|
-
expect(parsed.files_tracked).toBe(2);
|
|
1792
|
-
expect(parsed.summary).toContain("50%");
|
|
1793
|
-
expect(parsed).toHaveProperty("checkpoint_timestamp");
|
|
1794
|
-
|
|
1795
|
-
// Verify swarm_contexts table was updated
|
|
1796
|
-
const dbResult = await db.query<{
|
|
1797
|
-
id: string;
|
|
1798
|
-
epic_id: string;
|
|
1799
|
-
bead_id: string;
|
|
1800
|
-
strategy: string;
|
|
1801
|
-
files: string;
|
|
1802
|
-
recovery: string;
|
|
1803
|
-
}>(
|
|
1804
|
-
`SELECT id, epic_id, bead_id, strategy, files, recovery
|
|
1805
|
-
FROM swarm_contexts
|
|
1806
|
-
WHERE project_key = $1 AND bead_id = $2`,
|
|
1807
|
-
[uniqueProjectKey, beadId],
|
|
1808
|
-
);
|
|
1809
|
-
|
|
1810
|
-
expect(dbResult.rows.length).toBe(1);
|
|
1811
|
-
const row = dbResult.rows[0];
|
|
1812
|
-
expect(row.epic_id).toBe(epicId);
|
|
1813
|
-
expect(row.bead_id).toBe(beadId);
|
|
1814
|
-
expect(row.strategy).toBe("file-based");
|
|
1815
|
-
|
|
1816
|
-
// PGLite auto-parses JSON columns, so we get objects directly
|
|
1817
|
-
const files =
|
|
1818
|
-
typeof row.files === "string" ? JSON.parse(row.files) : row.files;
|
|
1819
|
-
expect(files).toEqual(["src/test.ts", "src/test2.ts"]);
|
|
1820
|
-
|
|
1821
|
-
const recovery =
|
|
1822
|
-
typeof row.recovery === "string"
|
|
1823
|
-
? JSON.parse(row.recovery)
|
|
1824
|
-
: row.recovery;
|
|
1825
|
-
expect(recovery.progress_percent).toBe(50);
|
|
1826
|
-
expect(recovery.files_modified).toEqual(["src/test.ts", "src/test2.ts"]);
|
|
1827
|
-
expect(recovery).toHaveProperty("last_checkpoint");
|
|
1828
|
-
} finally {
|
|
1829
|
-
await closeSwarmMailLibSQL(uniqueProjectKey);
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
it("handles checkpoint with error_context", async () => {
|
|
1834
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-checkpoint-error-${Date.now()}`;
|
|
1835
|
-
const sessionID = `checkpoint-error-session-${Date.now()}`;
|
|
1836
|
-
|
|
1837
|
-
const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
|
|
1838
|
-
const swarmMail = await getSwarmMailLibSQL(uniqueProjectKey);
|
|
1839
|
-
const db = await swarmMail.getDatabase();
|
|
1840
|
-
|
|
1841
|
-
try {
|
|
1842
|
-
const ctx = {
|
|
1843
|
-
...mockContext,
|
|
1844
|
-
sessionID,
|
|
1845
|
-
};
|
|
1846
|
-
|
|
1847
|
-
const result = await swarm_checkpoint.execute(
|
|
1848
|
-
{
|
|
1849
|
-
project_key: uniqueProjectKey,
|
|
1850
|
-
agent_name: "TestAgent",
|
|
1851
|
-
bead_id: "bd-error-test.1",
|
|
1852
|
-
epic_id: "bd-error-test",
|
|
1853
|
-
files_modified: ["src/buggy.ts"],
|
|
1854
|
-
progress_percent: 75,
|
|
1855
|
-
error_context:
|
|
1856
|
-
"Hit type error on line 42, need to add explicit types",
|
|
1857
|
-
},
|
|
1858
|
-
ctx,
|
|
1859
|
-
);
|
|
1860
|
-
|
|
1861
|
-
const parsed = JSON.parse(result);
|
|
1862
|
-
expect(parsed.success).toBe(true);
|
|
1863
|
-
|
|
1864
|
-
// Verify error_context was stored
|
|
1865
|
-
const dbResult = await db.query<{ recovery: string }>(
|
|
1866
|
-
`SELECT recovery FROM swarm_contexts WHERE project_key = $1 AND bead_id = $2`,
|
|
1867
|
-
[uniqueProjectKey, "bd-error-test.1"],
|
|
1868
|
-
);
|
|
1869
|
-
|
|
1870
|
-
const recoveryRaw = dbResult.rows[0].recovery;
|
|
1871
|
-
const recovery =
|
|
1872
|
-
typeof recoveryRaw === "string" ? JSON.parse(recoveryRaw) : recoveryRaw;
|
|
1873
|
-
expect(recovery.error_context).toBe(
|
|
1874
|
-
"Hit type error on line 42, need to add explicit types",
|
|
1875
|
-
);
|
|
1876
|
-
} finally {
|
|
1877
|
-
await closeSwarmMailLibSQL(uniqueProjectKey);
|
|
1878
|
-
}
|
|
1879
|
-
});
|
|
1880
|
-
});
|
|
1881
|
-
|
|
1882
|
-
describe("swarm_recover", () => {
|
|
1883
|
-
it("retrieves checkpoint data from swarm_contexts table", async () => {
|
|
1884
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-recover-${Date.now()}`;
|
|
1885
|
-
const sessionID = `recover-session-${Date.now()}`;
|
|
1886
|
-
|
|
1887
|
-
const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
|
|
1888
|
-
const swarmMail = await getSwarmMailLibSQL(uniqueProjectKey);
|
|
1889
|
-
const db = await swarmMail.getDatabase();
|
|
1890
|
-
|
|
1891
|
-
try {
|
|
1892
|
-
const ctx = {
|
|
1893
|
-
...mockContext,
|
|
1894
|
-
sessionID,
|
|
1895
|
-
};
|
|
1896
|
-
|
|
1897
|
-
const epicId = "bd-recover-epic-456";
|
|
1898
|
-
const beadId = "bd-recover-epic-456.1";
|
|
1899
|
-
const agentName = "TestAgent";
|
|
1900
|
-
|
|
1901
|
-
// First create a checkpoint
|
|
1902
|
-
await swarm_checkpoint.execute(
|
|
1903
|
-
{
|
|
1904
|
-
project_key: uniqueProjectKey,
|
|
1905
|
-
agent_name: agentName,
|
|
1906
|
-
bead_id: beadId,
|
|
1907
|
-
epic_id: epicId,
|
|
1908
|
-
files_modified: ["src/auth.ts", "src/middleware.ts"],
|
|
1909
|
-
progress_percent: 75,
|
|
1910
|
-
directives: {
|
|
1911
|
-
shared_context: "OAuth implementation in progress",
|
|
1912
|
-
skills_to_load: ["testing-patterns", "swarm-coordination"],
|
|
1913
|
-
},
|
|
1914
|
-
},
|
|
1915
|
-
ctx,
|
|
1916
|
-
);
|
|
1917
|
-
|
|
1918
|
-
// Now recover it
|
|
1919
|
-
const result = await swarm_recover.execute(
|
|
1920
|
-
{
|
|
1921
|
-
project_key: uniqueProjectKey,
|
|
1922
|
-
epic_id: epicId,
|
|
1923
|
-
},
|
|
1924
|
-
ctx,
|
|
1925
|
-
);
|
|
1926
|
-
|
|
1927
|
-
const parsed = JSON.parse(result);
|
|
1928
|
-
|
|
1929
|
-
// Verify recovery succeeded
|
|
1930
|
-
expect(parsed.found).toBe(true);
|
|
1931
|
-
expect(parsed).toHaveProperty("context");
|
|
1932
|
-
expect(parsed).toHaveProperty("summary");
|
|
1933
|
-
expect(parsed).toHaveProperty("age_seconds");
|
|
1934
|
-
|
|
1935
|
-
const { context } = parsed;
|
|
1936
|
-
expect(context.epic_id).toBe(epicId);
|
|
1937
|
-
expect(context.bead_id).toBe(beadId);
|
|
1938
|
-
expect(context.strategy).toBe("file-based");
|
|
1939
|
-
expect(context.files).toEqual(["src/auth.ts", "src/middleware.ts"]);
|
|
1940
|
-
expect(context.recovery.progress_percent).toBe(75);
|
|
1941
|
-
expect(context.directives.shared_context).toBe(
|
|
1942
|
-
"OAuth implementation in progress",
|
|
1943
|
-
);
|
|
1944
|
-
expect(context.directives.skills_to_load).toEqual([
|
|
1945
|
-
"testing-patterns",
|
|
1946
|
-
"swarm-coordination",
|
|
1947
|
-
]);
|
|
1948
|
-
} finally {
|
|
1949
|
-
await closeSwarmMailLibSQL(uniqueProjectKey);
|
|
1950
|
-
}
|
|
1951
|
-
});
|
|
1952
|
-
|
|
1953
|
-
it("returns found:false when no checkpoint exists", async () => {
|
|
1954
|
-
const uniqueProjectKey = `${TEST_PROJECT_PATH}-recover-notfound-${Date.now()}`;
|
|
1955
|
-
const sessionID = `recover-notfound-session-${Date.now()}`;
|
|
1956
|
-
|
|
1957
|
-
const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
|
|
1958
|
-
await getSwarmMailLibSQL(uniqueProjectKey);
|
|
1959
|
-
|
|
1960
|
-
try {
|
|
1961
|
-
const ctx = {
|
|
1962
|
-
...mockContext,
|
|
1963
|
-
sessionID,
|
|
1964
|
-
};
|
|
1965
|
-
|
|
1966
|
-
// Try to recover non-existent checkpoint
|
|
1967
|
-
const result = await swarm_recover.execute(
|
|
1968
|
-
{
|
|
1969
|
-
project_key: uniqueProjectKey,
|
|
1970
|
-
epic_id: "bd-nonexistent-epic",
|
|
1971
|
-
},
|
|
1972
|
-
ctx,
|
|
1973
|
-
);
|
|
1974
|
-
|
|
1975
|
-
const parsed = JSON.parse(result);
|
|
1976
|
-
|
|
1977
|
-
expect(parsed.found).toBe(false);
|
|
1978
|
-
expect(parsed.message).toContain("No checkpoint found");
|
|
1979
|
-
expect(parsed.epic_id).toBe("bd-nonexistent-epic");
|
|
1980
|
-
} finally {
|
|
1981
|
-
await closeSwarmMailLibSQL(uniqueProjectKey);
|
|
1982
|
-
}
|
|
1983
|
-
});
|
|
1984
|
-
});
|
|
1985
|
-
|
|
1986
|
-
// NOTE: Auto-checkpoint tests removed - they were flaky due to PGLite timing issues
|
|
1987
|
-
// in parallel test runs. The checkpoint functionality is tested via swarm_checkpoint
|
|
1988
|
-
// and swarm_recover tests above. Auto-checkpoint at milestones (25%, 50%, 75%) is
|
|
1989
|
-
// a convenience feature that doesn't need dedicated integration tests.
|
|
1990
|
-
});
|
|
1991
|
-
|
|
1992
|
-
// ============================================================================
|
|
1993
|
-
// Contract Validation Tests
|
|
1994
|
-
// ============================================================================
|
|
1995
|
-
|
|
1996
|
-
describe("Contract Validation", () => {
|
|
1997
|
-
describe("validateContract", () => {
|
|
1998
|
-
it("passes when files_touched is subset of files_owned", () => {
|
|
1999
|
-
// This test will fail until we implement validateContract
|
|
2000
|
-
const { validateContract } = require("./swarm-orchestrate");
|
|
2001
|
-
|
|
2002
|
-
const result = validateContract(
|
|
2003
|
-
["src/auth.ts", "src/utils.ts"],
|
|
2004
|
-
["src/auth.ts", "src/utils.ts", "src/types.ts"]
|
|
2005
|
-
);
|
|
2006
|
-
|
|
2007
|
-
expect(result.valid).toBe(true);
|
|
2008
|
-
expect(result.violations).toHaveLength(0);
|
|
2009
|
-
});
|
|
2010
|
-
|
|
2011
|
-
it("fails when files_touched has extra files", () => {
|
|
2012
|
-
const { validateContract } = require("./swarm-orchestrate");
|
|
2013
|
-
|
|
2014
|
-
const result = validateContract(
|
|
2015
|
-
["src/auth.ts", "src/forbidden.ts"],
|
|
2016
|
-
["src/auth.ts"]
|
|
2017
|
-
);
|
|
2018
|
-
|
|
2019
|
-
expect(result.valid).toBe(false);
|
|
2020
|
-
expect(result.violations).toContain("src/forbidden.ts");
|
|
2021
|
-
});
|
|
2022
|
-
|
|
2023
|
-
it("matches glob patterns correctly", () => {
|
|
2024
|
-
const { validateContract } = require("./swarm-orchestrate");
|
|
2025
|
-
|
|
2026
|
-
const result = validateContract(
|
|
2027
|
-
["src/auth/service.ts", "src/auth/types.ts"],
|
|
2028
|
-
["src/auth/**/*.ts"]
|
|
2029
|
-
);
|
|
2030
|
-
|
|
2031
|
-
expect(result.valid).toBe(true);
|
|
2032
|
-
expect(result.violations).toHaveLength(0);
|
|
2033
|
-
});
|
|
2034
|
-
|
|
2035
|
-
it("detects violations outside glob pattern", () => {
|
|
2036
|
-
const { validateContract } = require("./swarm-orchestrate");
|
|
2037
|
-
|
|
2038
|
-
const result = validateContract(
|
|
2039
|
-
["src/auth/service.ts", "src/utils/helper.ts"],
|
|
2040
|
-
["src/auth/**"]
|
|
2041
|
-
);
|
|
2042
|
-
|
|
2043
|
-
expect(result.valid).toBe(false);
|
|
2044
|
-
expect(result.violations).toContain("src/utils/helper.ts");
|
|
2045
|
-
});
|
|
2046
|
-
|
|
2047
|
-
it("passes with empty files_touched (read-only work)", () => {
|
|
2048
|
-
const { validateContract } = require("./swarm-orchestrate");
|
|
2049
|
-
|
|
2050
|
-
const result = validateContract(
|
|
2051
|
-
[],
|
|
2052
|
-
["src/auth/**"]
|
|
2053
|
-
);
|
|
2054
|
-
|
|
2055
|
-
expect(result.valid).toBe(true);
|
|
2056
|
-
expect(result.violations).toHaveLength(0);
|
|
2057
|
-
});
|
|
2058
|
-
|
|
2059
|
-
it("handles multiple glob patterns", () => {
|
|
2060
|
-
const { validateContract } = require("./swarm-orchestrate");
|
|
2061
|
-
|
|
2062
|
-
const result = validateContract(
|
|
2063
|
-
["src/auth/service.ts", "tests/auth.test.ts"],
|
|
2064
|
-
["src/auth/**", "tests/**"]
|
|
2065
|
-
);
|
|
2066
|
-
|
|
2067
|
-
expect(result.valid).toBe(true);
|
|
2068
|
-
expect(result.violations).toHaveLength(0);
|
|
2069
|
-
});
|
|
2070
|
-
});
|
|
2071
|
-
|
|
2072
|
-
describe("swarm_complete with contract validation", () => {
|
|
2073
|
-
it("includes contract validation result when files_touched provided", async () => {
|
|
2074
|
-
// This test needs a real decomposition event, so it's more of an integration check
|
|
2075
|
-
// The actual validation logic is tested in unit tests above
|
|
2076
|
-
// Here we just verify the response includes contract_validation field
|
|
2077
|
-
|
|
2078
|
-
const mockResult = {
|
|
2079
|
-
success: true,
|
|
2080
|
-
contract_validation: {
|
|
2081
|
-
validated: false,
|
|
2082
|
-
reason: "No files_owned contract found (non-epic subtask or decomposition event missing)",
|
|
2083
|
-
},
|
|
2084
|
-
};
|
|
2085
|
-
|
|
2086
|
-
// Verify the structure exists
|
|
2087
|
-
expect(mockResult.contract_validation).toBeDefined();
|
|
2088
|
-
expect(mockResult.contract_validation.validated).toBe(false);
|
|
2089
|
-
});
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
describe("swarm_complete project_key handling (bug fix)", () => {
|
|
2093
|
-
it("finds cells created with full path project_key", async () => {
|
|
2094
|
-
// BUG: swarm_complete was mangling project_key with .replace(/\//g, "-")
|
|
2095
|
-
// before querying, but cells are stored with the original path.
|
|
2096
|
-
// This caused "Bead not found" errors for cells created via hive_create_epic.
|
|
2097
|
-
|
|
2098
|
-
const testProjectPath = "/tmp/swarm-complete-projectkey-test-" + Date.now();
|
|
2099
|
-
const { getHiveAdapter } = await import("./hive");
|
|
2100
|
-
const adapter = await getHiveAdapter(testProjectPath);
|
|
2101
|
-
|
|
2102
|
-
// Create a cell using the full path as project_key (like hive_create_epic does)
|
|
2103
|
-
const cell = await adapter.createCell(testProjectPath, {
|
|
2104
|
-
title: "Test cell for project_key bug",
|
|
2105
|
-
type: "task",
|
|
2106
|
-
priority: 2,
|
|
2107
|
-
});
|
|
2108
|
-
|
|
2109
|
-
expect(cell.id).toBeDefined();
|
|
2110
|
-
|
|
2111
|
-
// Now try to complete it via swarm_complete with the same project_key
|
|
2112
|
-
const result = await swarm_complete.execute(
|
|
2113
|
-
{
|
|
2114
|
-
project_key: testProjectPath, // Full path, not mangled
|
|
2115
|
-
agent_name: "test-agent",
|
|
2116
|
-
bead_id: cell.id,
|
|
2117
|
-
summary: "Testing project_key handling",
|
|
2118
|
-
skip_verification: true,
|
|
2119
|
-
skip_review: true,
|
|
2120
|
-
},
|
|
2121
|
-
mockContext,
|
|
2122
|
-
);
|
|
2123
|
-
|
|
2124
|
-
const parsed = JSON.parse(result);
|
|
2125
|
-
|
|
2126
|
-
// This should succeed - the cell exists with this project_key
|
|
2127
|
-
// BUG: Before fix, this fails with "Bead not found" because swarm_complete
|
|
2128
|
-
// was looking for project_key "-tmp-swarm-complete-projectkey-test-xxx"
|
|
2129
|
-
expect(parsed.success).toBe(true);
|
|
2130
|
-
expect(parsed.error).toBeUndefined();
|
|
2131
|
-
expect(parsed.bead_id).toBe(cell.id);
|
|
2132
|
-
});
|
|
2133
|
-
|
|
2134
|
-
it("handles project_key with slashes correctly", async () => {
|
|
2135
|
-
// Verify that project_key like "/Users/joel/Code/project" works
|
|
2136
|
-
const testProjectPath = "/a/b/c/test-" + Date.now();
|
|
2137
|
-
const { getHiveAdapter } = await import("./hive");
|
|
2138
|
-
const adapter = await getHiveAdapter(testProjectPath);
|
|
2139
|
-
|
|
2140
|
-
const cell = await adapter.createCell(testProjectPath, {
|
|
2141
|
-
title: "Nested path test",
|
|
2142
|
-
type: "task",
|
|
2143
|
-
priority: 2,
|
|
2144
|
-
});
|
|
2145
|
-
|
|
2146
|
-
// Verify cell was created with correct project_key
|
|
2147
|
-
const retrieved = await adapter.getCell(testProjectPath, cell.id);
|
|
2148
|
-
expect(retrieved).not.toBeNull();
|
|
2149
|
-
expect(retrieved?.id).toBe(cell.id);
|
|
2150
|
-
|
|
2151
|
-
// swarm_complete should find it using the same project_key
|
|
2152
|
-
const result = await swarm_complete.execute(
|
|
2153
|
-
{
|
|
2154
|
-
project_key: testProjectPath,
|
|
2155
|
-
agent_name: "test-agent",
|
|
2156
|
-
bead_id: cell.id,
|
|
2157
|
-
summary: "Nested path test",
|
|
2158
|
-
skip_verification: true,
|
|
2159
|
-
skip_review: true,
|
|
2160
|
-
},
|
|
2161
|
-
mockContext,
|
|
2162
|
-
);
|
|
2163
|
-
|
|
2164
|
-
const parsed = JSON.parse(result);
|
|
2165
|
-
expect(parsed.success).toBe(true);
|
|
2166
|
-
});
|
|
2167
|
-
});
|
|
2168
|
-
|
|
2169
|
-
describe("swarm_complete review gate UX", () => {
|
|
2170
|
-
it("returns success: true with status: pending_review when review not attempted", async () => {
|
|
2171
|
-
const testProjectPath = "/tmp/swarm-review-gate-test-" + Date.now();
|
|
2172
|
-
const { getHiveAdapter } = await import("./hive");
|
|
2173
|
-
const adapter = await getHiveAdapter(testProjectPath);
|
|
2174
|
-
|
|
2175
|
-
// Create a task cell directly
|
|
2176
|
-
const cell = await adapter.createCell(testProjectPath, {
|
|
2177
|
-
title: "Test task for review gate",
|
|
2178
|
-
type: "task",
|
|
2179
|
-
priority: 2,
|
|
2180
|
-
});
|
|
2181
|
-
|
|
2182
|
-
// Start the task
|
|
2183
|
-
await adapter.updateCell(testProjectPath, cell.id, {
|
|
2184
|
-
status: "in_progress",
|
|
2185
|
-
});
|
|
2186
|
-
|
|
2187
|
-
// Try to complete without review (skip_review intentionally omitted - defaults to false)
|
|
2188
|
-
const result = await swarm_complete.execute(
|
|
2189
|
-
{
|
|
2190
|
-
project_key: testProjectPath,
|
|
2191
|
-
agent_name: "TestAgent",
|
|
2192
|
-
bead_id: cell.id,
|
|
2193
|
-
summary: "Done",
|
|
2194
|
-
files_touched: ["test.ts"],
|
|
2195
|
-
skip_verification: true,
|
|
2196
|
-
// skip_review intentionally omitted - defaults to false
|
|
2197
|
-
},
|
|
2198
|
-
mockContext,
|
|
2199
|
-
);
|
|
2200
|
-
|
|
2201
|
-
const parsed = JSON.parse(result);
|
|
2202
|
-
|
|
2203
|
-
// Should be success: true with workflow status
|
|
2204
|
-
expect(parsed.success).toBe(true);
|
|
2205
|
-
expect(parsed.status).toBe("pending_review");
|
|
2206
|
-
expect(parsed.message).toContain("awaiting coordinator review");
|
|
2207
|
-
expect(parsed.next_steps).toBeInstanceOf(Array);
|
|
2208
|
-
expect(parsed.next_steps.length).toBeGreaterThan(0);
|
|
2209
|
-
expect(parsed.review_status).toBeDefined();
|
|
2210
|
-
expect(parsed.review_status.reviewed).toBe(false);
|
|
2211
|
-
expect(parsed.review_status.approved).toBe(false);
|
|
2212
|
-
|
|
2213
|
-
// Should NOT have error field
|
|
2214
|
-
expect(parsed.error).toBeUndefined();
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
it("returns success: true, not error, when review not approved", async () => {
|
|
2218
|
-
const testProjectPath = "/tmp/swarm-review-not-approved-test-" + Date.now();
|
|
2219
|
-
const { getHiveAdapter } = await import("./hive");
|
|
2220
|
-
const { markReviewRejected } = await import("./swarm-review");
|
|
2221
|
-
const adapter = await getHiveAdapter(testProjectPath);
|
|
2222
|
-
|
|
2223
|
-
// Create a task cell directly
|
|
2224
|
-
const cell = await adapter.createCell(testProjectPath, {
|
|
2225
|
-
title: "Test task for review not approved",
|
|
2226
|
-
type: "task",
|
|
2227
|
-
priority: 2,
|
|
2228
|
-
});
|
|
2229
|
-
|
|
2230
|
-
// Start the task
|
|
2231
|
-
await adapter.updateCell(testProjectPath, cell.id, {
|
|
2232
|
-
status: "in_progress",
|
|
2233
|
-
});
|
|
2234
|
-
|
|
2235
|
-
// Manually set review status to rejected (approved: false, but reviewed: true)
|
|
2236
|
-
// This simulates the review gate detecting a review was done but not approved
|
|
2237
|
-
markReviewRejected(cell.id);
|
|
2238
|
-
|
|
2239
|
-
// Try to complete with review not approved
|
|
2240
|
-
const result = await swarm_complete.execute(
|
|
2241
|
-
{
|
|
2242
|
-
project_key: testProjectPath,
|
|
2243
|
-
agent_name: "TestAgent",
|
|
2244
|
-
bead_id: cell.id,
|
|
2245
|
-
summary: "Done",
|
|
2246
|
-
files_touched: ["test.ts"],
|
|
2247
|
-
skip_verification: true,
|
|
2248
|
-
},
|
|
2249
|
-
mockContext,
|
|
2250
|
-
);
|
|
2251
|
-
|
|
2252
|
-
const parsed = JSON.parse(result);
|
|
2253
|
-
|
|
2254
|
-
// Should be success: true with workflow status (not error)
|
|
2255
|
-
expect(parsed.success).toBe(true);
|
|
2256
|
-
expect(parsed.status).toBe("needs_changes");
|
|
2257
|
-
expect(parsed.message).toContain("changes requested");
|
|
2258
|
-
expect(parsed.next_steps).toBeInstanceOf(Array);
|
|
2259
|
-
expect(parsed.next_steps.length).toBeGreaterThan(0);
|
|
2260
|
-
expect(parsed.review_status).toBeDefined();
|
|
2261
|
-
expect(parsed.review_status.reviewed).toBe(true);
|
|
2262
|
-
expect(parsed.review_status.approved).toBe(false);
|
|
2263
|
-
|
|
2264
|
-
// Should NOT have error field
|
|
2265
|
-
expect(parsed.error).toBeUndefined();
|
|
2266
|
-
});
|
|
2267
|
-
|
|
2268
|
-
it("completes successfully when skip_review=true", async () => {
|
|
2269
|
-
const testProjectPath = "/tmp/swarm-skip-review-test-" + Date.now();
|
|
2270
|
-
const { getHiveAdapter } = await import("./hive");
|
|
2271
|
-
const adapter = await getHiveAdapter(testProjectPath);
|
|
2272
|
-
|
|
2273
|
-
// Create a task cell directly
|
|
2274
|
-
const cell = await adapter.createCell(testProjectPath, {
|
|
2275
|
-
title: "Test task for skip review",
|
|
2276
|
-
type: "task",
|
|
2277
|
-
priority: 2,
|
|
2278
|
-
});
|
|
2279
|
-
|
|
2280
|
-
// Start the task
|
|
2281
|
-
await adapter.updateCell(testProjectPath, cell.id, {
|
|
2282
|
-
status: "in_progress",
|
|
2283
|
-
});
|
|
2284
|
-
|
|
2285
|
-
// Complete with skip_review
|
|
2286
|
-
const result = await swarm_complete.execute(
|
|
2287
|
-
{
|
|
2288
|
-
project_key: testProjectPath,
|
|
2289
|
-
agent_name: "TestAgent",
|
|
2290
|
-
bead_id: cell.id,
|
|
2291
|
-
summary: "Done",
|
|
2292
|
-
files_touched: ["test.ts"],
|
|
2293
|
-
skip_verification: true,
|
|
2294
|
-
skip_review: true,
|
|
2295
|
-
},
|
|
2296
|
-
mockContext,
|
|
2297
|
-
);
|
|
2298
|
-
|
|
2299
|
-
const parsed = JSON.parse(result);
|
|
2300
|
-
|
|
2301
|
-
// Should complete without review gate
|
|
2302
|
-
expect(parsed.success).toBe(true);
|
|
2303
|
-
expect(parsed.status).toBeUndefined(); // No workflow status when skipping
|
|
2304
|
-
expect(parsed.error).toBeUndefined();
|
|
2305
|
-
});
|
|
2306
|
-
});
|
|
2307
|
-
|
|
2308
|
-
describe("swarm_complete auto-sync", () => {
|
|
2309
|
-
it("calls hive_sync after closing cell on successful completion", async () => {
|
|
2310
|
-
const testProjectPath = "/tmp/swarm-auto-sync-test-" + Date.now();
|
|
2311
|
-
const { getHiveAdapter } = await import("./hive");
|
|
2312
|
-
const adapter = await getHiveAdapter(testProjectPath);
|
|
2313
|
-
|
|
2314
|
-
// Create a task cell directly
|
|
2315
|
-
const cell = await adapter.createCell(testProjectPath, {
|
|
2316
|
-
title: "Test task for auto-sync",
|
|
2317
|
-
type: "task",
|
|
2318
|
-
priority: 2,
|
|
2319
|
-
});
|
|
2320
|
-
|
|
2321
|
-
// Start the task
|
|
2322
|
-
await adapter.updateCell(testProjectPath, cell.id, {
|
|
2323
|
-
status: "in_progress",
|
|
2324
|
-
});
|
|
2325
|
-
|
|
2326
|
-
// Complete with skip_review and skip_verification
|
|
2327
|
-
const result = await swarm_complete.execute(
|
|
2328
|
-
{
|
|
2329
|
-
project_key: testProjectPath,
|
|
2330
|
-
agent_name: "TestAgent",
|
|
2331
|
-
bead_id: cell.id,
|
|
2332
|
-
summary: "Done - testing auto-sync",
|
|
2333
|
-
files_touched: [],
|
|
2334
|
-
skip_verification: true,
|
|
2335
|
-
skip_review: true,
|
|
2336
|
-
},
|
|
2337
|
-
mockContext,
|
|
2338
|
-
);
|
|
2339
|
-
|
|
2340
|
-
const parsed = JSON.parse(result);
|
|
2341
|
-
|
|
2342
|
-
// Should complete successfully
|
|
2343
|
-
expect(parsed.success).toBe(true);
|
|
2344
|
-
expect(parsed.closed).toBe(true);
|
|
2345
|
-
|
|
2346
|
-
// Check that cell is actually closed in database
|
|
2347
|
-
const closedCell = await adapter.getCell(testProjectPath, cell.id);
|
|
2348
|
-
expect(closedCell?.status).toBe("closed");
|
|
2349
|
-
|
|
2350
|
-
// The sync should have flushed the cell to .hive/issues.jsonl
|
|
2351
|
-
// We can verify the cell appears in the JSONL
|
|
2352
|
-
const hivePath = `${testProjectPath}/.hive/issues.jsonl`;
|
|
2353
|
-
const hiveFile = Bun.file(hivePath);
|
|
2354
|
-
const exists = await hiveFile.exists();
|
|
2355
|
-
|
|
2356
|
-
// The file should exist after sync
|
|
2357
|
-
expect(exists).toBe(true);
|
|
2358
|
-
|
|
2359
|
-
if (exists) {
|
|
2360
|
-
const content = await hiveFile.text();
|
|
2361
|
-
const lines = content.trim().split("\n");
|
|
2362
|
-
|
|
2363
|
-
// Should have at least one cell exported
|
|
2364
|
-
expect(lines.length).toBeGreaterThan(0);
|
|
2365
|
-
|
|
2366
|
-
// Parse the exported cells to find our closed cell
|
|
2367
|
-
const cells = lines.map((line) => JSON.parse(line));
|
|
2368
|
-
const exportedCell = cells.find((c) => c.id === cell.id);
|
|
2369
|
-
|
|
2370
|
-
// Our cell should be in the export
|
|
2371
|
-
expect(exportedCell).toBeDefined();
|
|
2372
|
-
expect(exportedCell.status).toBe("closed");
|
|
2373
|
-
expect(exportedCell.title).toBe("Test task for auto-sync");
|
|
2374
|
-
}
|
|
2375
|
-
});
|
|
2376
|
-
});
|
|
2377
|
-
});
|