opencode-swarm-plugin 0.36.1 → 0.37.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.
@@ -0,0 +1,195 @@
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, mock } 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
+ // Mock captureDecomposition to spy on calls
45
+ const captureDecompositionSpy = mock(() => ({
46
+ id: "test-epic-123",
47
+ timestamp: new Date().toISOString(),
48
+ task: "Add user authentication",
49
+ }));
50
+ const original = evalCapture.captureDecomposition;
51
+ // @ts-expect-error - mocking for test
52
+ evalCapture.captureDecomposition = captureDecompositionSpy;
53
+
54
+ const validCellTree = JSON.stringify({
55
+ epic: {
56
+ title: "Add OAuth",
57
+ description: "Implement OAuth authentication",
58
+ },
59
+ subtasks: [
60
+ {
61
+ title: "Add OAuth provider config",
62
+ description: "Set up Google OAuth",
63
+ files: ["src/auth/google.ts", "src/auth/config.ts"],
64
+ dependencies: [],
65
+ estimated_complexity: 2,
66
+ },
67
+ {
68
+ title: "Add login UI",
69
+ description: "Create login button component",
70
+ files: ["src/components/LoginButton.tsx"],
71
+ dependencies: [0],
72
+ estimated_complexity: 1,
73
+ },
74
+ ],
75
+ });
76
+
77
+ const result = await swarm_validate_decomposition.execute(
78
+ {
79
+ response: validCellTree,
80
+ project_path: testProjectPath,
81
+ task: "Add user authentication",
82
+ context: "Using NextAuth.js",
83
+ strategy: "feature-based" as const,
84
+ epic_id: "test-epic-123",
85
+ },
86
+ mockContext,
87
+ );
88
+
89
+ const parsed = JSON.parse(result);
90
+ expect(parsed.valid).toBe(true);
91
+
92
+ // Verify captureDecomposition was called with correct params
93
+ expect(captureDecompositionSpy).toHaveBeenCalledTimes(1);
94
+ const callArgs = captureDecompositionSpy.mock.calls[0][0];
95
+ expect(callArgs.epicId).toBe("test-epic-123");
96
+ expect(callArgs.projectPath).toBe(testProjectPath);
97
+ expect(callArgs.task).toBe("Add user authentication");
98
+ expect(callArgs.context).toBe("Using NextAuth.js");
99
+ expect(callArgs.strategy).toBe("feature-based");
100
+ expect(callArgs.epicTitle).toBe("Add OAuth");
101
+ expect(callArgs.epicDescription).toBe("Implement OAuth authentication");
102
+ expect(callArgs.subtasks).toHaveLength(2);
103
+ expect(callArgs.subtasks[0].title).toBe("Add OAuth provider config");
104
+
105
+ // Restore
106
+ // @ts-expect-error - restoring mock
107
+ evalCapture.captureDecomposition = original;
108
+ });
109
+
110
+ test("does not call captureDecomposition when validation fails", async () => {
111
+ const captureDecompositionSpy = mock(() => ({}));
112
+ const original = evalCapture.captureDecomposition;
113
+ // @ts-expect-error - mocking for test
114
+ evalCapture.captureDecomposition = captureDecompositionSpy;
115
+
116
+ // Invalid CellTree - missing required fields
117
+ const invalidCellTree = JSON.stringify({
118
+ epic: { title: "Missing subtasks" },
119
+ // No subtasks array
120
+ });
121
+
122
+ const result = await swarm_validate_decomposition.execute(
123
+ {
124
+ response: invalidCellTree,
125
+ project_path: testProjectPath,
126
+ task: "Add auth",
127
+ strategy: "auto" as const,
128
+ epic_id: "test-epic-456",
129
+ },
130
+ mockContext,
131
+ );
132
+
133
+ const parsed = JSON.parse(result);
134
+ expect(parsed.valid).toBe(false);
135
+
136
+ // Verify captureDecomposition was NOT called
137
+ expect(captureDecompositionSpy).not.toHaveBeenCalled();
138
+
139
+ // Restore
140
+ // @ts-expect-error - restoring mock
141
+ evalCapture.captureDecomposition = original;
142
+ });
143
+
144
+ test("handles optional context and description fields", async () => {
145
+ const captureDecompositionSpy = mock(() => ({
146
+ id: "test-epic-789",
147
+ timestamp: new Date().toISOString(),
148
+ task: "Fix the auth bug",
149
+ }));
150
+ const original = evalCapture.captureDecomposition;
151
+ // @ts-expect-error - mocking for test
152
+ evalCapture.captureDecomposition = captureDecompositionSpy;
153
+
154
+ const validCellTree = JSON.stringify({
155
+ epic: {
156
+ title: "Fix bug",
157
+ // No description
158
+ },
159
+ subtasks: [
160
+ {
161
+ title: "Add test",
162
+ files: ["src/test.ts"],
163
+ dependencies: [],
164
+ estimated_complexity: 1,
165
+ },
166
+ ],
167
+ });
168
+
169
+ const result = await swarm_validate_decomposition.execute(
170
+ {
171
+ response: validCellTree,
172
+ project_path: testProjectPath,
173
+ task: "Fix the auth bug",
174
+ // No context
175
+ strategy: "risk-based" as const,
176
+ epic_id: "test-epic-789",
177
+ },
178
+ mockContext,
179
+ );
180
+
181
+ const parsed = JSON.parse(result);
182
+ expect(parsed.valid).toBe(true);
183
+
184
+ // Verify captureDecomposition was called without optional fields
185
+ expect(captureDecompositionSpy).toHaveBeenCalledTimes(1);
186
+ const callArgs = captureDecompositionSpy.mock.calls[0][0];
187
+ expect(callArgs.epicId).toBe("test-epic-789");
188
+ expect(callArgs.context).toBeUndefined();
189
+ expect(callArgs.epicDescription).toBeUndefined();
190
+
191
+ // Restore
192
+ // @ts-expect-error - restoring mock
193
+ evalCapture.captureDecomposition = original;
194
+ });
195
+ });
@@ -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,
@@ -2,18 +2,15 @@
2
2
  * Integration tests for swarm review feedback flow
3
3
  *
4
4
  * Tests the coordinator review feedback workflow with real HiveAdapter and swarm-mail.
5
- * Verifies that review approval/rejection properly updates state and sends messages.
5
+ * Verifies that review approval/rejection properly updates state.
6
6
  *
7
- * **STATUS**: URL_INVALID bug FIXED by commit 7bf9385 (libSQL URL normalization).
8
- * Tests now execute without URL errors. sendSwarmMessage successfully creates adapters.
9
- *
10
- * **REMAINING ISSUE**: Message retrieval not working. getInbox returns empty even though
11
- * sendSwarmMessage succeeds. Possible causes:
12
- * - Database adapter instance mismatch (sendSwarmMessage creates new adapter each call)
13
- * - Message projection not materializing from events
14
- * - Database path resolution issue between send and receive
15
- *
16
- * Tests currently SKIPPED pending message retrieval fix.
7
+ * **ARCHITECTURE**: Coordinator-driven retry pattern (swarm_spawn_retry)
8
+ * - `approved` status: Sends message to worker (worker can complete)
9
+ * - `needs_changes` status: NO message sent (worker is dead, coordinator spawns retry)
10
+ * - After 3 rejections: Task marked blocked, NO message sent
11
+ *
12
+ * This aligns with the "worker is dead" philosophy - failed reviews require coordinator
13
+ * intervention via swarm_spawn_retry, not worker self-retry.
17
14
  */
18
15
 
19
16
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
@@ -195,23 +192,23 @@ describe("swarm_review integration", () => {
195
192
  expect(feedbackParsed.attempt).toBe(1);
196
193
  expect(feedbackParsed.remaining_attempts).toBe(2);
197
194
 
198
- // Verify retry count incremented
199
- expect(feedbackParsed.attempt).toBe(1);
200
-
201
- // Verify message was sent with issues
195
+ // Verify retry_context is provided for coordinator to spawn retry
196
+ expect(feedbackParsed.retry_context).toBeDefined();
197
+ expect(feedbackParsed.retry_context.task_id).toBe(subtask.id);
198
+ expect(feedbackParsed.retry_context.attempt).toBe(1);
199
+ expect(feedbackParsed.retry_context.max_attempts).toBe(3);
200
+ expect(feedbackParsed.retry_context.issues).toEqual(issues);
201
+ expect(feedbackParsed.retry_context.next_action).toContain("swarm_spawn_retry");
202
+
203
+ // ARCHITECTURE CHANGE: No longer sends message to worker
204
+ // Worker is considered "dead" - coordinator must spawn retry
205
+ // Inbox should remain empty
202
206
  const messages = await swarmMail.getInbox(
203
207
  testProjectPath,
204
208
  "TestWorker",
205
209
  { limit: 10 }
206
210
  );
207
- expect(messages.length).toBeGreaterThan(0);
208
-
209
- const needsChangesMessage = messages.find((m) =>
210
- m.subject.includes("NEEDS CHANGES")
211
- );
212
- expect(needsChangesMessage).toBeDefined();
213
- expect(needsChangesMessage?.subject).toContain(subtask.id);
214
- expect(needsChangesMessage?.subject).toContain("attempt 1/3");
211
+ expect(messages.length).toBe(0);
215
212
  });
216
213
 
217
214
  test("3-strike rule: task marked blocked after 3 rejections", async () => {
@@ -275,16 +272,14 @@ describe("swarm_review integration", () => {
275
272
  const updatedCell = await hive.getCell(testProjectPath, subtask.id);
276
273
  expect(updatedCell?.status).toBe("blocked");
277
274
 
278
- // Verify final failure message was sent
275
+ // ARCHITECTURE CHANGE: No longer sends failure message
276
+ // Worker is dead, coordinator handles escalation
277
+ // Inbox should remain empty
279
278
  const messages = await swarmMail.getInbox(
280
279
  testProjectPath,
281
280
  "TestWorker",
282
281
  { limit: 10 }
283
282
  );
284
-
285
- const failedMessage = messages.find((m) => m.subject.includes("FAILED"));
286
- expect(failedMessage).toBeDefined();
287
- expect(failedMessage?.subject).toContain("max review attempts reached");
288
- expect(failedMessage?.importance).toBe("urgent");
283
+ expect(messages.length).toBe(0);
289
284
  });
290
285
  });