opencode-swarm-plugin 0.36.1 → 0.38.0
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/.hive/issues.jsonl +16 -0
- package/.hive/memories.jsonl +13 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +286 -286
- package/CHANGELOG.md +170 -0
- package/README.md +33 -0
- package/bin/swarm.test.ts +106 -0
- package/bin/swarm.ts +181 -208
- package/dist/hive.d.ts +59 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +453 -118
- package/dist/plugin.js +452 -118
- package/dist/swarm-decompose.d.ts +30 -0
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm.d.ts +15 -0
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +27 -10
- package/examples/plugin-wrapper-template.ts +60 -8
- package/package.json +4 -1
- package/src/compaction-hook.test.ts +97 -2
- package/src/compaction-hook.ts +32 -2
- package/src/hive.integration.test.ts +148 -0
- package/src/hive.ts +89 -0
- package/src/swarm-decompose.test.ts +188 -0
- package/src/swarm-decompose.ts +52 -1
- package/src/swarm-orchestrate.test.ts +270 -7
- package/src/swarm-orchestrate.ts +98 -11
- package/src/swarm-prompts.test.ts +121 -0
- package/src/swarm-prompts.ts +295 -2
- package/src/swarm-research.integration.test.ts +157 -0
- package/src/swarm-review.integration.test.ts +24 -29
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Decompose Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for task decomposition, validation, and eval capture integration.
|
|
5
|
+
*
|
|
6
|
+
* TDD: Testing eval capture integration - verifies captureDecomposition() is called
|
|
7
|
+
* after successful validation with correct parameters.
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test, spyOn } from "bun:test";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import { swarm_validate_decomposition } from "./swarm-decompose";
|
|
12
|
+
import * as evalCapture from "./eval-capture.js";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Test Setup
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const mockContext = {
|
|
19
|
+
sessionID: `test-decompose-${Date.now()}`,
|
|
20
|
+
messageID: `test-message-${Date.now()}`,
|
|
21
|
+
agent: "test-agent",
|
|
22
|
+
abort: new AbortController().signal,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let testProjectPath: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
testProjectPath = `/tmp/test-swarm-decompose-${Date.now()}`;
|
|
29
|
+
fs.mkdirSync(testProjectPath, { recursive: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (fs.existsSync(testProjectPath)) {
|
|
34
|
+
fs.rmSync(testProjectPath, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Eval Capture Integration Tests
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
describe("captureDecomposition integration", () => {
|
|
43
|
+
test("calls captureDecomposition after successful validation with all params", async () => {
|
|
44
|
+
// Spy on captureDecomposition
|
|
45
|
+
const captureDecompositionSpy = spyOn(evalCapture, "captureDecomposition");
|
|
46
|
+
|
|
47
|
+
const validCellTree = JSON.stringify({
|
|
48
|
+
epic: {
|
|
49
|
+
title: "Add OAuth",
|
|
50
|
+
description: "Implement OAuth authentication",
|
|
51
|
+
},
|
|
52
|
+
subtasks: [
|
|
53
|
+
{
|
|
54
|
+
title: "Add OAuth provider config",
|
|
55
|
+
description: "Set up Google OAuth",
|
|
56
|
+
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
57
|
+
dependencies: [],
|
|
58
|
+
estimated_complexity: 2,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: "Add login UI",
|
|
62
|
+
description: "Create login button component",
|
|
63
|
+
files: ["src/components/LoginButton.tsx"],
|
|
64
|
+
dependencies: [0],
|
|
65
|
+
estimated_complexity: 1,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await swarm_validate_decomposition.execute(
|
|
71
|
+
{
|
|
72
|
+
response: validCellTree,
|
|
73
|
+
project_path: testProjectPath,
|
|
74
|
+
task: "Add user authentication",
|
|
75
|
+
context: "Using NextAuth.js",
|
|
76
|
+
strategy: "feature-based" as const,
|
|
77
|
+
epic_id: "test-epic-123",
|
|
78
|
+
},
|
|
79
|
+
mockContext,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const parsed = JSON.parse(result);
|
|
83
|
+
expect(parsed.valid).toBe(true);
|
|
84
|
+
|
|
85
|
+
// Verify captureDecomposition was called with correct params
|
|
86
|
+
expect(captureDecompositionSpy).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(captureDecompositionSpy).toHaveBeenCalledWith({
|
|
88
|
+
epicId: "test-epic-123",
|
|
89
|
+
projectPath: testProjectPath,
|
|
90
|
+
task: "Add user authentication",
|
|
91
|
+
context: "Using NextAuth.js",
|
|
92
|
+
strategy: "feature-based",
|
|
93
|
+
epicTitle: "Add OAuth",
|
|
94
|
+
epicDescription: "Implement OAuth authentication",
|
|
95
|
+
subtasks: [
|
|
96
|
+
{
|
|
97
|
+
title: "Add OAuth provider config",
|
|
98
|
+
description: "Set up Google OAuth",
|
|
99
|
+
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
100
|
+
dependencies: [],
|
|
101
|
+
estimated_complexity: 2,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: "Add login UI",
|
|
105
|
+
description: "Create login button component",
|
|
106
|
+
files: ["src/components/LoginButton.tsx"],
|
|
107
|
+
dependencies: [0],
|
|
108
|
+
estimated_complexity: 1,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
captureDecompositionSpy.mockRestore();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("does not call captureDecomposition when validation fails", async () => {
|
|
117
|
+
const captureDecompositionSpy = spyOn(evalCapture, "captureDecomposition");
|
|
118
|
+
|
|
119
|
+
// Invalid CellTree - missing required fields
|
|
120
|
+
const invalidCellTree = JSON.stringify({
|
|
121
|
+
epic: { title: "Missing subtasks" },
|
|
122
|
+
// No subtasks array
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await swarm_validate_decomposition.execute(
|
|
126
|
+
{
|
|
127
|
+
response: invalidCellTree,
|
|
128
|
+
project_path: testProjectPath,
|
|
129
|
+
task: "Add auth",
|
|
130
|
+
strategy: "auto" as const,
|
|
131
|
+
epic_id: "test-epic-456",
|
|
132
|
+
},
|
|
133
|
+
mockContext,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const parsed = JSON.parse(result);
|
|
137
|
+
expect(parsed.valid).toBe(false);
|
|
138
|
+
|
|
139
|
+
// Verify captureDecomposition was NOT called
|
|
140
|
+
expect(captureDecompositionSpy).not.toHaveBeenCalled();
|
|
141
|
+
|
|
142
|
+
captureDecompositionSpy.mockRestore();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("handles optional context and description fields", async () => {
|
|
146
|
+
const captureDecompositionSpy = spyOn(evalCapture, "captureDecomposition");
|
|
147
|
+
|
|
148
|
+
const validCellTree = JSON.stringify({
|
|
149
|
+
epic: {
|
|
150
|
+
title: "Fix bug",
|
|
151
|
+
// No description
|
|
152
|
+
},
|
|
153
|
+
subtasks: [
|
|
154
|
+
{
|
|
155
|
+
title: "Add test",
|
|
156
|
+
files: ["src/test.ts"],
|
|
157
|
+
dependencies: [],
|
|
158
|
+
estimated_complexity: 1,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await swarm_validate_decomposition.execute(
|
|
164
|
+
{
|
|
165
|
+
response: validCellTree,
|
|
166
|
+
project_path: testProjectPath,
|
|
167
|
+
task: "Fix the auth bug",
|
|
168
|
+
// No context
|
|
169
|
+
strategy: "risk-based" as const,
|
|
170
|
+
epic_id: "test-epic-789",
|
|
171
|
+
},
|
|
172
|
+
mockContext,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const parsed = JSON.parse(result);
|
|
176
|
+
expect(parsed.valid).toBe(true);
|
|
177
|
+
|
|
178
|
+
// Verify captureDecomposition was called without optional fields
|
|
179
|
+
expect(captureDecompositionSpy).toHaveBeenCalledTimes(1);
|
|
180
|
+
const call = captureDecompositionSpy.mock.calls[0];
|
|
181
|
+
expect(call[0].epicId).toBe("test-epic-789");
|
|
182
|
+
expect(call[0].context).toBeUndefined();
|
|
183
|
+
// Schema default makes description empty string instead of undefined
|
|
184
|
+
expect(call[0].epicDescription).toBe("");
|
|
185
|
+
|
|
186
|
+
captureDecompositionSpy.mockRestore();
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/swarm-decompose.ts
CHANGED
|
@@ -535,11 +535,31 @@ export const swarm_decompose = tool({
|
|
|
535
535
|
* Use this after the agent responds to swarm:decompose to validate the structure.
|
|
536
536
|
*/
|
|
537
537
|
export const swarm_validate_decomposition = tool({
|
|
538
|
-
description: "Validate a decomposition response against CellTreeSchema",
|
|
538
|
+
description: "Validate a decomposition response against CellTreeSchema and capture for eval",
|
|
539
539
|
args: {
|
|
540
540
|
response: tool.schema
|
|
541
541
|
.string()
|
|
542
542
|
.describe("JSON response from agent (CellTree format)"),
|
|
543
|
+
project_path: tool.schema
|
|
544
|
+
.string()
|
|
545
|
+
.optional()
|
|
546
|
+
.describe("Project path for eval capture"),
|
|
547
|
+
task: tool.schema
|
|
548
|
+
.string()
|
|
549
|
+
.optional()
|
|
550
|
+
.describe("Original task description for eval capture"),
|
|
551
|
+
context: tool.schema
|
|
552
|
+
.string()
|
|
553
|
+
.optional()
|
|
554
|
+
.describe("Context provided for decomposition"),
|
|
555
|
+
strategy: tool.schema
|
|
556
|
+
.enum(["file-based", "feature-based", "risk-based", "auto"])
|
|
557
|
+
.optional()
|
|
558
|
+
.describe("Decomposition strategy used"),
|
|
559
|
+
epic_id: tool.schema
|
|
560
|
+
.string()
|
|
561
|
+
.optional()
|
|
562
|
+
.describe("Epic ID for eval capture"),
|
|
543
563
|
},
|
|
544
564
|
async execute(args) {
|
|
545
565
|
try {
|
|
@@ -597,6 +617,37 @@ export const swarm_validate_decomposition = tool({
|
|
|
597
617
|
validated.subtasks,
|
|
598
618
|
);
|
|
599
619
|
|
|
620
|
+
// Capture decomposition for eval if all required params provided
|
|
621
|
+
if (
|
|
622
|
+
args.project_path &&
|
|
623
|
+
args.task &&
|
|
624
|
+
args.strategy &&
|
|
625
|
+
args.epic_id
|
|
626
|
+
) {
|
|
627
|
+
try {
|
|
628
|
+
const { captureDecomposition } = await import("./eval-capture.js");
|
|
629
|
+
captureDecomposition({
|
|
630
|
+
epicId: args.epic_id,
|
|
631
|
+
projectPath: args.project_path,
|
|
632
|
+
task: args.task,
|
|
633
|
+
context: args.context,
|
|
634
|
+
strategy: args.strategy,
|
|
635
|
+
epicTitle: validated.epic.title,
|
|
636
|
+
epicDescription: validated.epic.description,
|
|
637
|
+
subtasks: validated.subtasks.map((s) => ({
|
|
638
|
+
title: s.title,
|
|
639
|
+
description: s.description,
|
|
640
|
+
files: s.files,
|
|
641
|
+
dependencies: s.dependencies,
|
|
642
|
+
estimated_complexity: s.estimated_complexity,
|
|
643
|
+
})),
|
|
644
|
+
});
|
|
645
|
+
} catch (error) {
|
|
646
|
+
// Non-fatal - don't block validation if capture fails
|
|
647
|
+
console.warn("[swarm_validate_decomposition] Failed to capture decomposition:", error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
600
651
|
return JSON.stringify(
|
|
601
652
|
{
|
|
602
653
|
valid: true,
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* - Researcher spawning for identified technologies
|
|
7
7
|
* - Summary collection from semantic-memory
|
|
8
8
|
* - Research result aggregation
|
|
9
|
+
* - Eval capture integration (captureSubtaskOutcome wiring)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import { describe, test, expect, beforeEach } from "bun:test";
|
|
12
|
-
import { runResearchPhase, extractTechStack } from "./swarm-orchestrate";
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
|
13
|
+
import { runResearchPhase, extractTechStack, swarm_complete } from "./swarm-orchestrate";
|
|
14
|
+
import * as evalCapture from "./eval-capture.js";
|
|
15
|
+
import * as fs from "node:fs";
|
|
13
16
|
|
|
14
17
|
describe("extractTechStack", () => {
|
|
15
18
|
test("extracts Next.js from task description", () => {
|
|
@@ -115,9 +118,269 @@ describe("runResearchPhase", () => {
|
|
|
115
118
|
});
|
|
116
119
|
});
|
|
117
120
|
|
|
118
|
-
describe("swarm_research_phase tool", () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// describe("swarm_research_phase tool", () => {
|
|
122
|
+
// test.todo("exposes research phase as plugin tool");
|
|
123
|
+
// test.todo("validates task parameter");
|
|
124
|
+
// test.todo("validates project_path parameter");
|
|
125
|
+
// test.todo("returns JSON string with research results");
|
|
126
|
+
// });
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Eval Capture Integration Tests (swarm_complete)
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
describe("captureSubtaskOutcome integration", () => {
|
|
133
|
+
const mockContext = {
|
|
134
|
+
sessionID: `test-complete-${Date.now()}`,
|
|
135
|
+
messageID: `test-message-${Date.now()}`,
|
|
136
|
+
agent: "test-agent",
|
|
137
|
+
abort: new AbortController().signal,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let testProjectPath: string;
|
|
141
|
+
|
|
142
|
+
beforeEach(async () => {
|
|
143
|
+
testProjectPath = `/tmp/test-swarm-complete-${Date.now()}`;
|
|
144
|
+
fs.mkdirSync(testProjectPath, { recursive: true });
|
|
145
|
+
|
|
146
|
+
// Create .hive directory and issues.jsonl
|
|
147
|
+
const hiveDir = `${testProjectPath}/.hive`;
|
|
148
|
+
fs.mkdirSync(hiveDir, { recursive: true });
|
|
149
|
+
fs.writeFileSync(`${hiveDir}/issues.jsonl`, "", "utf-8");
|
|
150
|
+
|
|
151
|
+
// Set hive working directory to testProjectPath
|
|
152
|
+
const { setHiveWorkingDirectory } = await import("./hive");
|
|
153
|
+
setHiveWorkingDirectory(testProjectPath);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
if (fs.existsSync(testProjectPath)) {
|
|
158
|
+
fs.rmSync(testProjectPath, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("calls captureSubtaskOutcome after successful completion with all params", async () => {
|
|
163
|
+
// Import hive tools
|
|
164
|
+
const { hive_create_epic } = await import("./hive");
|
|
165
|
+
|
|
166
|
+
// Spy on captureSubtaskOutcome
|
|
167
|
+
const captureOutcomeSpy = spyOn(evalCapture, "captureSubtaskOutcome");
|
|
168
|
+
|
|
169
|
+
// Create an epic with a subtask using hive_create_epic
|
|
170
|
+
const epicResult = await hive_create_epic.execute({
|
|
171
|
+
epic_title: "Add OAuth",
|
|
172
|
+
epic_description: "Implement OAuth authentication",
|
|
173
|
+
subtasks: [
|
|
174
|
+
{
|
|
175
|
+
title: "Add auth service",
|
|
176
|
+
priority: 2,
|
|
177
|
+
files: ["src/auth/service.ts", "src/auth/schema.ts"],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}, mockContext);
|
|
181
|
+
|
|
182
|
+
const epicData = JSON.parse(epicResult);
|
|
183
|
+
expect(epicData.success).toBe(true);
|
|
184
|
+
|
|
185
|
+
const epicId = epicData.epic.id;
|
|
186
|
+
const beadId = epicData.subtasks[0].id;
|
|
187
|
+
|
|
188
|
+
const startTime = Date.now() - 120000; // Started 2 minutes ago
|
|
189
|
+
const plannedFiles = ["src/auth/service.ts", "src/auth/schema.ts"];
|
|
190
|
+
const actualFiles = ["src/auth/service.ts", "src/auth/schema.ts", "src/auth/types.ts"];
|
|
191
|
+
|
|
192
|
+
// Call swarm_complete
|
|
193
|
+
const result = await swarm_complete.execute(
|
|
194
|
+
{
|
|
195
|
+
project_key: testProjectPath,
|
|
196
|
+
agent_name: "TestAgent",
|
|
197
|
+
bead_id: beadId,
|
|
198
|
+
summary: "Implemented OAuth service with JWT strategy",
|
|
199
|
+
files_touched: actualFiles,
|
|
200
|
+
skip_verification: true, // Skip verification for test
|
|
201
|
+
skip_review: true, // Skip review for test
|
|
202
|
+
planned_files: plannedFiles,
|
|
203
|
+
start_time: startTime,
|
|
204
|
+
error_count: 0,
|
|
205
|
+
retry_count: 0,
|
|
206
|
+
},
|
|
207
|
+
mockContext,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const parsed = JSON.parse(result);
|
|
211
|
+
expect(parsed.success).toBe(true);
|
|
212
|
+
|
|
213
|
+
// Verify captureSubtaskOutcome was called with correct params
|
|
214
|
+
expect(captureOutcomeSpy).toHaveBeenCalledTimes(1);
|
|
215
|
+
|
|
216
|
+
const call = captureOutcomeSpy.mock.calls[0][0];
|
|
217
|
+
expect(call.epicId).toBe(epicId);
|
|
218
|
+
expect(call.projectPath).toBe(testProjectPath);
|
|
219
|
+
expect(call.beadId).toBe(beadId);
|
|
220
|
+
expect(call.title).toBe("Add auth service");
|
|
221
|
+
expect(call.plannedFiles).toEqual(plannedFiles);
|
|
222
|
+
expect(call.actualFiles).toEqual(actualFiles);
|
|
223
|
+
expect(call.durationMs).toBeGreaterThan(0);
|
|
224
|
+
expect(call.errorCount).toBe(0);
|
|
225
|
+
expect(call.retryCount).toBe(0);
|
|
226
|
+
expect(call.success).toBe(true);
|
|
227
|
+
|
|
228
|
+
captureOutcomeSpy.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("does not call captureSubtaskOutcome when required params missing", async () => {
|
|
232
|
+
const { hive_create_epic } = await import("./hive");
|
|
233
|
+
const captureOutcomeSpy = spyOn(evalCapture, "captureSubtaskOutcome");
|
|
234
|
+
|
|
235
|
+
// Create an epic with a subtask
|
|
236
|
+
const epicResult = await hive_create_epic.execute({
|
|
237
|
+
epic_title: "Fix bug",
|
|
238
|
+
subtasks: [
|
|
239
|
+
{
|
|
240
|
+
title: "Fix auth bug",
|
|
241
|
+
priority: 1,
|
|
242
|
+
files: ["src/auth.ts"],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}, mockContext);
|
|
246
|
+
|
|
247
|
+
const epicData = JSON.parse(epicResult);
|
|
248
|
+
const beadId = epicData.subtasks[0].id;
|
|
249
|
+
|
|
250
|
+
// Call without planned_files or start_time
|
|
251
|
+
const result = await swarm_complete.execute(
|
|
252
|
+
{
|
|
253
|
+
project_key: testProjectPath,
|
|
254
|
+
agent_name: "TestAgent",
|
|
255
|
+
bead_id: beadId,
|
|
256
|
+
summary: "Fixed the bug",
|
|
257
|
+
skip_verification: true,
|
|
258
|
+
skip_review: true,
|
|
259
|
+
// No planned_files, start_time
|
|
260
|
+
},
|
|
261
|
+
mockContext,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const parsed = JSON.parse(result);
|
|
265
|
+
expect(parsed.success).toBe(true);
|
|
266
|
+
|
|
267
|
+
// Capture should still be called, but with default values
|
|
268
|
+
// (The function is called in all success cases, it just handles missing params)
|
|
269
|
+
expect(captureOutcomeSpy).toHaveBeenCalledTimes(1);
|
|
270
|
+
|
|
271
|
+
captureOutcomeSpy.mockRestore();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Eval Capture Integration Tests (swarm_record_outcome)
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
describe("finalizeEvalRecord integration", () => {
|
|
280
|
+
const mockContext = {
|
|
281
|
+
sessionID: `test-finalize-${Date.now()}`,
|
|
282
|
+
messageID: `test-message-${Date.now()}`,
|
|
283
|
+
agent: "test-agent",
|
|
284
|
+
abort: new AbortController().signal,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
test("calls finalizeEvalRecord when project_path and epic_id provided", async () => {
|
|
288
|
+
const { swarm_record_outcome } = await import("./swarm-orchestrate");
|
|
289
|
+
|
|
290
|
+
// Spy on finalizeEvalRecord
|
|
291
|
+
const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
|
|
292
|
+
finalizeEvalSpy.mockReturnValue(null); // Mock return value
|
|
293
|
+
|
|
294
|
+
const testProjectPath = "/tmp/test-project";
|
|
295
|
+
const testEpicId = "bd-test123";
|
|
296
|
+
const testBeadId = `${testEpicId}.0`;
|
|
297
|
+
|
|
298
|
+
// Call swarm_record_outcome with epic_id and project_path
|
|
299
|
+
await swarm_record_outcome.execute({
|
|
300
|
+
bead_id: testBeadId,
|
|
301
|
+
duration_ms: 120000,
|
|
302
|
+
error_count: 0,
|
|
303
|
+
retry_count: 0,
|
|
304
|
+
success: true,
|
|
305
|
+
files_touched: ["src/test.ts"],
|
|
306
|
+
epic_id: testEpicId,
|
|
307
|
+
project_path: testProjectPath,
|
|
308
|
+
}, mockContext);
|
|
309
|
+
|
|
310
|
+
// Verify finalizeEvalRecord was called
|
|
311
|
+
expect(finalizeEvalSpy).toHaveBeenCalledTimes(1);
|
|
312
|
+
expect(finalizeEvalSpy).toHaveBeenCalledWith({
|
|
313
|
+
epicId: testEpicId,
|
|
314
|
+
projectPath: testProjectPath,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
finalizeEvalSpy.mockRestore();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("does not call finalizeEvalRecord when epic_id or project_path missing", async () => {
|
|
321
|
+
const { swarm_record_outcome } = await import("./swarm-orchestrate");
|
|
322
|
+
|
|
323
|
+
// Spy on finalizeEvalRecord
|
|
324
|
+
const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
|
|
325
|
+
|
|
326
|
+
const testBeadId = "bd-test123.0";
|
|
327
|
+
|
|
328
|
+
// Call without epic_id or project_path
|
|
329
|
+
await swarm_record_outcome.execute({
|
|
330
|
+
bead_id: testBeadId,
|
|
331
|
+
duration_ms: 120000,
|
|
332
|
+
error_count: 0,
|
|
333
|
+
retry_count: 0,
|
|
334
|
+
success: true,
|
|
335
|
+
}, mockContext);
|
|
336
|
+
|
|
337
|
+
// Verify finalizeEvalRecord was NOT called
|
|
338
|
+
expect(finalizeEvalSpy).toHaveBeenCalledTimes(0);
|
|
339
|
+
|
|
340
|
+
finalizeEvalSpy.mockRestore();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("includes finalized record in response when available", async () => {
|
|
344
|
+
const { swarm_record_outcome } = await import("./swarm-orchestrate");
|
|
345
|
+
|
|
346
|
+
// Mock finalizeEvalRecord to return a record
|
|
347
|
+
const mockFinalRecord = {
|
|
348
|
+
id: "bd-test123",
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
project_path: "/tmp/test-project",
|
|
351
|
+
task: "Test task",
|
|
352
|
+
strategy: "file-based" as const,
|
|
353
|
+
subtask_count: 2,
|
|
354
|
+
epic_title: "Test Epic",
|
|
355
|
+
subtasks: [],
|
|
356
|
+
overall_success: true,
|
|
357
|
+
total_duration_ms: 240000,
|
|
358
|
+
total_errors: 0,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
|
|
362
|
+
finalizeEvalSpy.mockReturnValue(mockFinalRecord);
|
|
363
|
+
|
|
364
|
+
const testProjectPath = "/tmp/test-project";
|
|
365
|
+
const testEpicId = "bd-test123";
|
|
366
|
+
const testBeadId = `${testEpicId}.0`;
|
|
367
|
+
|
|
368
|
+
// Call with epic_id and project_path
|
|
369
|
+
const result = await swarm_record_outcome.execute({
|
|
370
|
+
bead_id: testBeadId,
|
|
371
|
+
duration_ms: 120000,
|
|
372
|
+
error_count: 0,
|
|
373
|
+
retry_count: 0,
|
|
374
|
+
success: true,
|
|
375
|
+
epic_id: testEpicId,
|
|
376
|
+
project_path: testProjectPath,
|
|
377
|
+
}, mockContext);
|
|
378
|
+
|
|
379
|
+
// Parse result and check for finalized record
|
|
380
|
+
const parsed = JSON.parse(result);
|
|
381
|
+
expect(parsed).toHaveProperty("finalized_eval_record");
|
|
382
|
+
expect(parsed.finalized_eval_record).toEqual(mockFinalRecord);
|
|
383
|
+
|
|
384
|
+
finalizeEvalSpy.mockRestore();
|
|
385
|
+
});
|
|
123
386
|
});
|