opencode-swarm-plugin 0.12.26 → 0.12.28

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.
@@ -1,558 +0,0 @@
1
- ---
2
- name: testing-strategies
3
- description: Testing patterns for TypeScript/Bun projects with vitest. Use when writing unit tests, integration tests, or testing async/swarm operations. Covers mocking, test organization, fixtures, and testing MCP tools.
4
- tags:
5
- - testing
6
- - vitest
7
- - typescript
8
- - integration
9
- tools:
10
- - Read
11
- - Write
12
- - Bash
13
- ---
14
-
15
- # Testing Strategies
16
-
17
- Testing patterns for TypeScript projects using vitest. Focused on unit tests, integration tests, and testing async/distributed operations.
18
-
19
- ## Test Organization
20
-
21
- ### File Naming
22
-
23
- **Unit tests**: `*.test.ts` alongside source
24
-
25
- ```
26
- src/
27
- skills.ts
28
- skills.test.ts
29
- ```
30
-
31
- **Integration tests**: `*.integration.test.ts` with separate config
32
-
33
- ```
34
- src/
35
- swarm.ts
36
- swarm.integration.test.ts
37
- vitest.integration.config.ts
38
- ```
39
-
40
- ### Test Structure
41
-
42
- Use `describe` blocks for logical grouping. Nest deeply when needed.
43
-
44
- ```typescript
45
- describe("parseFrontmatter", () => {
46
- it("parses valid frontmatter with all fields", () => {
47
- const result = parseFrontmatter(VALID_SKILL_MD);
48
- expect(result).not.toBeNull();
49
- expect(result.metadata.name).toBe("test-skill");
50
- });
51
-
52
- it("returns null for missing name value", () => {
53
- const result = parseFrontmatter(INVALID_FRONTMATTER_MD);
54
- expect(result.metadata.name).toBeNull();
55
- });
56
- });
57
- ```
58
-
59
- ### Lifecycle Hooks
60
-
61
- **beforeEach/afterEach**: Reset state between tests
62
-
63
- ```typescript
64
- describe("discoverSkills", () => {
65
- beforeEach(() => {
66
- cleanupTestSkillsDir();
67
- setupTestSkillsDir();
68
- invalidateSkillsCache(); // Clear caches!
69
- });
70
-
71
- afterEach(() => {
72
- cleanupTestSkillsDir();
73
- invalidateSkillsCache();
74
- });
75
- });
76
- ```
77
-
78
- **beforeAll/afterAll**: Setup/teardown shared resources
79
-
80
- ```typescript
81
- describe("swarm_status", () => {
82
- let beadsAvailable = false;
83
-
84
- beforeAll(async () => {
85
- beadsAvailable = await isBeadsAvailable();
86
- });
87
- });
88
- ```
89
-
90
- ## Vitest Basics
91
-
92
- ### Assertions
93
-
94
- ```typescript
95
- // Equality
96
- expect(result).toBe(expected);
97
- expect(result).toEqual(expected); // Deep equality for objects
98
-
99
- // Truthiness
100
- expect(result).toBeTruthy();
101
- expect(result).toBeFalsy();
102
- expect(result).toBeDefined();
103
- expect(result).toBeNull();
104
-
105
- // Collections
106
- expect(array).toContain(item);
107
- expect(array).toHaveLength(3);
108
- expect(object).toHaveProperty("key");
109
- expect(object).toHaveProperty("key", "value");
110
-
111
- // Strings
112
- expect(string).toContain("substring");
113
- expect(string).toMatch(/regex/);
114
-
115
- // Numbers
116
- expect(num).toBeGreaterThan(5);
117
- expect(num).toBeGreaterThanOrEqual(5);
118
- expect(num).toBeLessThan(10);
119
-
120
- // Exceptions
121
- expect(() => dangerousFn()).toThrow();
122
- expect(() => dangerousFn()).toThrow("specific message");
123
- ```
124
-
125
- ### Conditional Tests
126
-
127
- **Skip tests when dependencies unavailable**:
128
-
129
- ```typescript
130
- it.skipIf(!agentMailAvailable)("reports progress to Agent Mail", async () => {
131
- // Test that requires Agent Mail
132
- });
133
- ```
134
-
135
- **Only run specific tests**:
136
-
137
- ```typescript
138
- it.only("focus on this test", () => {
139
- // Only this test runs
140
- });
141
- ```
142
-
143
- ## Test Fixtures
144
-
145
- ### Unique Temp Directories
146
-
147
- Avoid collisions between test runs:
148
-
149
- ```typescript
150
- const TEST_RUN_ID = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
151
- const TEST_DIR = join(process.cwd(), `.test-skills-${TEST_RUN_ID}`);
152
- ```
153
-
154
- ### Setup/Teardown Helpers
155
-
156
- ```typescript
157
- function setupTestSkillsDir() {
158
- mkdirSync(SKILLS_DIR, { recursive: true });
159
- mkdirSync(join(SKILLS_DIR, "test-skill"), { recursive: true });
160
- writeFileSync(join(SKILLS_DIR, "test-skill", "SKILL.md"), VALID_SKILL_MD);
161
- }
162
-
163
- function cleanupTestSkillsDir() {
164
- if (existsSync(TEST_DIR)) {
165
- rmSync(TEST_DIR, { recursive: true, force: true });
166
- }
167
- }
168
- ```
169
-
170
- ### Fixture Constants
171
-
172
- Keep test data at top of file for reuse:
173
-
174
- ```typescript
175
- const VALID_SKILL_MD = `---
176
- name: test-skill
177
- description: A test skill for unit testing
178
- tags:
179
- - testing
180
- ---
181
-
182
- # Test Skill
183
-
184
- Content here.
185
- `;
186
-
187
- const MINIMAL_SKILL_MD = `---
188
- name: minimal-skill
189
- description: Minimal skill
190
- ---
191
-
192
- Just the basics.
193
- `;
194
- ```
195
-
196
- ## Mocking Patterns
197
-
198
- ### Mock External Services
199
-
200
- ```typescript
201
- const mockContext = {
202
- sessionID: `test-session-${Date.now()}`,
203
- messageID: `test-message-${Date.now()}`,
204
- agent: "test-agent",
205
- abort: new AbortController().signal,
206
- };
207
- ```
208
-
209
- ### Check Service Availability
210
-
211
- ```typescript
212
- async function isAgentMailAvailable(): Promise<boolean> {
213
- try {
214
- const url = process.env.AGENT_MAIL_URL || AGENT_MAIL_URL;
215
- const response = await fetch(`${url}/health/liveness`);
216
- return response.ok;
217
- } catch {
218
- return false;
219
- }
220
- }
221
-
222
- async function isBeadsAvailable(): Promise<boolean> {
223
- try {
224
- const result = await Bun.$`bd --version`.quiet().nothrow();
225
- return result.exitCode === 0;
226
- } catch {
227
- return false;
228
- }
229
- }
230
- ```
231
-
232
- ### Conditional Test Execution
233
-
234
- ```typescript
235
- describe("swarm_status (integration)", () => {
236
- let beadsAvailable = false;
237
-
238
- beforeAll(async () => {
239
- beadsAvailable = await isBeadsAvailable();
240
- });
241
-
242
- it.skipIf(!beadsAvailable)("returns status for epic", async () => {
243
- // Test logic
244
- });
245
- });
246
- ```
247
-
248
- ## Testing Async Operations
249
-
250
- ### Promises
251
-
252
- ```typescript
253
- it("generates valid decomposition prompt", async () => {
254
- const result = await swarm_decompose.execute(
255
- {
256
- task: "Add user authentication",
257
- max_subtasks: 3,
258
- },
259
- mockContext,
260
- );
261
-
262
- const parsed = JSON.parse(result);
263
- expect(parsed).toHaveProperty("prompt");
264
- });
265
- ```
266
-
267
- ### Error Handling
268
-
269
- ```typescript
270
- it("rejects invalid JSON", async () => {
271
- const result = await swarm_validate_decomposition.execute(
272
- { response: "not valid json {" },
273
- mockContext,
274
- );
275
-
276
- const parsed = JSON.parse(result);
277
- expect(parsed.valid).toBe(false);
278
- expect(parsed.error).toContain("Invalid JSON");
279
- });
280
-
281
- it("throws for non-existent epic", async () => {
282
- try {
283
- await swarm_status.execute(
284
- {
285
- epic_id: "bd-nonexistent",
286
- project_key: TEST_PROJECT_PATH,
287
- },
288
- mockContext,
289
- );
290
- } catch (error) {
291
- expect(error).toBeInstanceOf(Error);
292
- if (error instanceof Error && "operation" in error) {
293
- expect((error as { operation: string }).operation).toBe("query_subtasks");
294
- }
295
- }
296
- });
297
- ```
298
-
299
- ## Testing Zod Schemas
300
-
301
- ### Valid Cases
302
-
303
- ```typescript
304
- it("validates a complete bead", () => {
305
- const bead = {
306
- id: "bd-abc123",
307
- title: "Fix the thing",
308
- type: "bug",
309
- status: "open",
310
- priority: 1,
311
- created_at: "2025-01-01T00:00:00Z",
312
- updated_at: "2025-01-01T00:00:00Z",
313
- };
314
- expect(() => BeadSchema.parse(bead)).not.toThrow();
315
- });
316
- ```
317
-
318
- ### Invalid Cases
319
-
320
- ```typescript
321
- it("rejects invalid priority", () => {
322
- const bead = {
323
- id: "bd-abc123",
324
- title: "Fix the thing",
325
- type: "bug",
326
- status: "open",
327
- priority: 5, // Invalid: max is 3
328
- created_at: "2025-01-01T00:00:00Z",
329
- updated_at: "2025-01-01T00:00:00Z",
330
- };
331
- expect(() => BeadSchema.parse(bead)).toThrow();
332
- });
333
- ```
334
-
335
- ### Enum Validation
336
-
337
- ```typescript
338
- it("accepts all valid types", () => {
339
- const types = ["bug", "feature", "task", "epic", "chore"];
340
- for (const type of types) {
341
- expect(() => BeadTypeSchema.parse(type)).not.toThrow();
342
- }
343
- });
344
- ```
345
-
346
- ### Default Values
347
-
348
- ```typescript
349
- it("validates minimal create args with defaults", () => {
350
- const args = { title: "New bead" };
351
- const result = BeadCreateArgsSchema.parse(args);
352
- expect(result.title).toBe("New bead");
353
- expect(result.type).toBe("task"); // default
354
- expect(result.priority).toBe(2); // default
355
- });
356
- ```
357
-
358
- ## Integration Test Config
359
-
360
- Create separate vitest config for integration tests:
361
-
362
- ```typescript
363
- // vitest.integration.config.ts
364
- import { defineConfig } from "vitest/config";
365
-
366
- export default defineConfig({
367
- test: {
368
- include: ["src/**/*.integration.test.ts"],
369
- testTimeout: 30000, // Integration tests may be slower
370
- hookTimeout: 30000,
371
- // Run serially to avoid race conditions
372
- sequence: {
373
- concurrent: false,
374
- },
375
- },
376
- });
377
- ```
378
-
379
- Run with:
380
-
381
- ```bash
382
- vitest --config vitest.integration.config.ts
383
- ```
384
-
385
- ## Testing MCP Tools
386
-
387
- ### Tool Execute Pattern
388
-
389
- MCP tools implement `execute(args, context)`:
390
-
391
- ```typescript
392
- const result = await swarm_decompose.execute(
393
- {
394
- task: "Add OAuth authentication",
395
- max_subtasks: 3,
396
- },
397
- mockContext,
398
- );
399
-
400
- const parsed = JSON.parse(result);
401
- expect(parsed).toHaveProperty("prompt");
402
- ```
403
-
404
- ### JSON Output Validation
405
-
406
- Many tools return JSON strings:
407
-
408
- ```typescript
409
- it("returns expected schema", async () => {
410
- const result = await swarm_plan_prompt.execute(
411
- {
412
- task: "Some task",
413
- max_subtasks: 5,
414
- query_cass: false,
415
- },
416
- mockContext,
417
- );
418
-
419
- const parsed = JSON.parse(result);
420
- expect(parsed).toHaveProperty("expected_schema", "BeadTree");
421
- expect(parsed).toHaveProperty("validation_note");
422
- expect(parsed.schema_hint).toHaveProperty("epic");
423
- expect(parsed.schema_hint).toHaveProperty("subtasks");
424
- });
425
- ```
426
-
427
- ## Edge Cases
428
-
429
- Test boundary conditions:
430
-
431
- ```typescript
432
- describe("edge cases", () => {
433
- it("handles non-existent directory gracefully", async () => {
434
- setSkillsProjectDirectory("/non/existent/path");
435
- invalidateSkillsCache();
436
-
437
- const skills = await discoverSkills();
438
- expect(skills instanceof Map).toBe(true);
439
- });
440
-
441
- it("handles empty skills directory", async () => {
442
- mkdirSync(SKILLS_DIR, { recursive: true });
443
- setSkillsProjectDirectory(TEST_DIR);
444
- invalidateSkillsCache();
445
-
446
- const skills = await discoverSkills();
447
- expect(skills instanceof Map).toBe(true);
448
- });
449
-
450
- it("returns null for empty name", async () => {
451
- const skill = await getSkill("");
452
- expect(skill).toBeNull();
453
- });
454
- });
455
- ```
456
-
457
- ## End-to-End Tests
458
-
459
- Full workflow tests combining multiple operations:
460
-
461
- ```typescript
462
- it("creates epic, reports progress, completes subtask", async () => {
463
- // 1. Setup
464
- await mcpCall("ensure_project", { human_key: uniqueProjectKey });
465
- const agent = await mcpCall("register_agent", {
466
- project_key: uniqueProjectKey,
467
- program: "opencode-test",
468
- model: "test",
469
- });
470
-
471
- // 2. Create epic
472
- const epicResult = await Bun.$`bd create "Feature" -t epic --json`.quiet();
473
- const epic = JSON.parse(epicResult.stdout.toString());
474
-
475
- // 3. Report progress
476
- await swarm_progress.execute(
477
- {
478
- project_key: uniqueProjectKey,
479
- agent_name: agent.name,
480
- bead_id: subtask.id,
481
- status: "in_progress",
482
- },
483
- ctx,
484
- );
485
-
486
- // 4. Complete
487
- const result = await swarm_complete.execute(
488
- {
489
- project_key: uniqueProjectKey,
490
- agent_name: agent.name,
491
- bead_id: subtask.id,
492
- summary: "Done",
493
- },
494
- ctx,
495
- );
496
-
497
- expect(result.success).toBe(true);
498
- });
499
- ```
500
-
501
- ## Common Patterns
502
-
503
- ### Test Data Validation
504
-
505
- ```typescript
506
- it("includes confidence score and reasoning", async () => {
507
- const result = await swarm_select_strategy.execute(
508
- { task: "Implement dashboard" },
509
- mockContext,
510
- );
511
- const parsed = JSON.parse(result);
512
-
513
- expect(parsed).toHaveProperty("strategy");
514
- expect(parsed).toHaveProperty("confidence");
515
- expect(typeof parsed.confidence).toBe("number");
516
- expect(parsed.confidence).toBeGreaterThanOrEqual(0);
517
- expect(parsed.confidence).toBeLessThanOrEqual(1);
518
- });
519
- ```
520
-
521
- ### Array/Collection Testing
522
-
523
- ```typescript
524
- it("includes alternatives with scores", async () => {
525
- const result = await swarm_select_strategy.execute(
526
- { task: "Build module" },
527
- mockContext,
528
- );
529
- const parsed = JSON.parse(result);
530
-
531
- expect(parsed.alternatives).toBeInstanceOf(Array);
532
- expect(parsed.alternatives.length).toBe(2);
533
-
534
- for (const alt of parsed.alternatives) {
535
- expect(alt).toHaveProperty("strategy");
536
- expect(alt).toHaveProperty("score");
537
- expect(typeof alt.score).toBe("number");
538
- }
539
- });
540
- ```
541
-
542
- ### String Content Testing
543
-
544
- ```typescript
545
- it("includes context in prompt when provided", async () => {
546
- const result = await swarm_decompose.execute(
547
- {
548
- task: "Refactor API",
549
- context: "Using Next.js App Router",
550
- },
551
- mockContext,
552
- );
553
-
554
- const parsed = JSON.parse(result);
555
- expect(parsed.prompt).toContain("Next.js App Router");
556
- expect(parsed.prompt).toContain("Additional Context");
557
- });
558
- ```