opencode-swarm-plugin 0.1.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,688 @@
1
+ /**
2
+ * Beads Integration Tests
3
+ *
4
+ * These tests exercise the real `bd` CLI in a Docker environment.
5
+ * They validate the tool wrappers work correctly with actual beads operations.
6
+ *
7
+ * Run with: bun run docker:test
8
+ */
9
+ import { describe, it, expect, beforeAll, beforeEach, afterAll } from "vitest";
10
+ import {
11
+ beads_create,
12
+ beads_create_epic,
13
+ beads_query,
14
+ beads_update,
15
+ beads_close,
16
+ beads_start,
17
+ beads_ready,
18
+ beads_link_thread,
19
+ BeadError,
20
+ } from "./beads";
21
+ import type { Bead, EpicCreateResult } from "./schemas";
22
+
23
+ /**
24
+ * Mock tool context for execute functions
25
+ * The real context is provided by OpenCode runtime
26
+ */
27
+ const mockContext = {
28
+ sessionID: "test-session-" + Date.now(),
29
+ messageID: "test-message-" + Date.now(),
30
+ agent: "test-agent",
31
+ abort: new AbortController().signal,
32
+ };
33
+
34
+ /**
35
+ * Helper to parse JSON response from tool execute
36
+ */
37
+ function parseResponse<T>(response: string): T {
38
+ return JSON.parse(response) as T;
39
+ }
40
+
41
+ /**
42
+ * Track created beads for cleanup
43
+ */
44
+ const createdBeadIds: string[] = [];
45
+
46
+ /**
47
+ * Cleanup helper - close all created beads after tests
48
+ */
49
+ async function cleanupBeads() {
50
+ for (const id of createdBeadIds) {
51
+ try {
52
+ await beads_close.execute({ id, reason: "Test cleanup" }, mockContext);
53
+ } catch {
54
+ // Ignore cleanup errors - bead may already be closed
55
+ }
56
+ }
57
+ createdBeadIds.length = 0;
58
+ }
59
+
60
+ describe("beads integration", () => {
61
+ // Verify bd CLI is available before running tests
62
+ beforeAll(async () => {
63
+ const result = await Bun.$`bd --version`.quiet().nothrow();
64
+ if (result.exitCode !== 0) {
65
+ throw new Error(
66
+ "bd CLI not found. Run tests in Docker with: bun run docker:test",
67
+ );
68
+ }
69
+ });
70
+
71
+ afterAll(async () => {
72
+ await cleanupBeads();
73
+ });
74
+
75
+ describe("beads_create", () => {
76
+ it("creates a bead with minimal args (title only)", async () => {
77
+ const result = await beads_create.execute(
78
+ { title: "Test bead minimal" },
79
+ mockContext,
80
+ );
81
+
82
+ const bead = parseResponse<Bead>(result);
83
+ createdBeadIds.push(bead.id);
84
+
85
+ expect(bead.title).toBe("Test bead minimal");
86
+ expect(bead.status).toBe("open");
87
+ expect(bead.issue_type).toBe("task"); // default
88
+ expect(bead.priority).toBe(2); // default
89
+ expect(bead.id).toMatch(/^[a-z0-9-]+-[a-z0-9]+$/);
90
+ });
91
+
92
+ it("creates a bead with all options", async () => {
93
+ const result = await beads_create.execute(
94
+ {
95
+ title: "Test bug with priority",
96
+ type: "bug",
97
+ priority: 0, // P0 critical
98
+ description: "This is a critical bug",
99
+ },
100
+ mockContext,
101
+ );
102
+
103
+ const bead = parseResponse<Bead>(result);
104
+ createdBeadIds.push(bead.id);
105
+
106
+ expect(bead.title).toBe("Test bug with priority");
107
+ expect(bead.issue_type).toBe("bug");
108
+ expect(bead.priority).toBe(0);
109
+ expect(bead.description).toContain("critical bug");
110
+ });
111
+
112
+ it("creates a feature type bead", async () => {
113
+ const result = await beads_create.execute(
114
+ { title: "New feature request", type: "feature", priority: 1 },
115
+ mockContext,
116
+ );
117
+
118
+ const bead = parseResponse<Bead>(result);
119
+ createdBeadIds.push(bead.id);
120
+
121
+ expect(bead.issue_type).toBe("feature");
122
+ expect(bead.priority).toBe(1);
123
+ });
124
+
125
+ it("creates a chore type bead", async () => {
126
+ const result = await beads_create.execute(
127
+ { title: "Cleanup task", type: "chore", priority: 3 },
128
+ mockContext,
129
+ );
130
+
131
+ const bead = parseResponse<Bead>(result);
132
+ createdBeadIds.push(bead.id);
133
+
134
+ expect(bead.issue_type).toBe("chore");
135
+ expect(bead.priority).toBe(3);
136
+ });
137
+ });
138
+
139
+ describe("beads_query", () => {
140
+ let testBeadId: string;
141
+
142
+ beforeEach(async () => {
143
+ // Create a test bead for query tests
144
+ const result = await beads_create.execute(
145
+ { title: "Query test bead", type: "task" },
146
+ mockContext,
147
+ );
148
+ const bead = parseResponse<Bead>(result);
149
+ testBeadId = bead.id;
150
+ createdBeadIds.push(testBeadId);
151
+ });
152
+
153
+ it("queries all open beads", async () => {
154
+ const result = await beads_query.execute({ status: "open" }, mockContext);
155
+
156
+ const beads = parseResponse<Bead[]>(result);
157
+
158
+ expect(Array.isArray(beads)).toBe(true);
159
+ expect(beads.length).toBeGreaterThan(0);
160
+ expect(beads.every((b) => b.status === "open")).toBe(true);
161
+ });
162
+
163
+ it("queries beads by type", async () => {
164
+ const result = await beads_query.execute({ type: "task" }, mockContext);
165
+
166
+ const beads = parseResponse<Bead[]>(result);
167
+
168
+ expect(Array.isArray(beads)).toBe(true);
169
+ expect(beads.every((b) => b.issue_type === "task")).toBe(true);
170
+ });
171
+
172
+ it("queries ready beads (unblocked)", async () => {
173
+ const result = await beads_query.execute({ ready: true }, mockContext);
174
+
175
+ const beads = parseResponse<Bead[]>(result);
176
+
177
+ expect(Array.isArray(beads)).toBe(true);
178
+ // Ready beads should be open (not closed, not blocked)
179
+ for (const bead of beads) {
180
+ expect(["open", "in_progress"]).toContain(bead.status);
181
+ }
182
+ });
183
+
184
+ it("limits results", async () => {
185
+ // Create multiple beads first
186
+ for (let i = 0; i < 5; i++) {
187
+ const result = await beads_create.execute(
188
+ { title: `Limit test bead ${i}` },
189
+ mockContext,
190
+ );
191
+ const bead = parseResponse<Bead>(result);
192
+ createdBeadIds.push(bead.id);
193
+ }
194
+
195
+ const result = await beads_query.execute({ limit: 3 }, mockContext);
196
+
197
+ const beads = parseResponse<Bead[]>(result);
198
+ expect(beads.length).toBeLessThanOrEqual(3);
199
+ });
200
+
201
+ it("combines filters", async () => {
202
+ const result = await beads_query.execute(
203
+ { status: "open", type: "task", limit: 5 },
204
+ mockContext,
205
+ );
206
+
207
+ const beads = parseResponse<Bead[]>(result);
208
+
209
+ expect(Array.isArray(beads)).toBe(true);
210
+ expect(beads.length).toBeLessThanOrEqual(5);
211
+ for (const bead of beads) {
212
+ expect(bead.status).toBe("open");
213
+ expect(bead.issue_type).toBe("task");
214
+ }
215
+ });
216
+ });
217
+
218
+ describe("beads_update", () => {
219
+ let testBeadId: string;
220
+
221
+ beforeEach(async () => {
222
+ const result = await beads_create.execute(
223
+ { title: "Update test bead", description: "Original description" },
224
+ mockContext,
225
+ );
226
+ const bead = parseResponse<Bead>(result);
227
+ testBeadId = bead.id;
228
+ createdBeadIds.push(testBeadId);
229
+ });
230
+
231
+ it("updates bead status", async () => {
232
+ const result = await beads_update.execute(
233
+ { id: testBeadId, status: "in_progress" },
234
+ mockContext,
235
+ );
236
+
237
+ const bead = parseResponse<Bead>(result);
238
+ expect(bead.status).toBe("in_progress");
239
+ });
240
+
241
+ it("updates bead description", async () => {
242
+ const result = await beads_update.execute(
243
+ { id: testBeadId, description: "Updated description" },
244
+ mockContext,
245
+ );
246
+
247
+ const bead = parseResponse<Bead>(result);
248
+ expect(bead.description).toContain("Updated description");
249
+ });
250
+
251
+ it("updates bead priority", async () => {
252
+ const result = await beads_update.execute(
253
+ { id: testBeadId, priority: 0 },
254
+ mockContext,
255
+ );
256
+
257
+ const bead = parseResponse<Bead>(result);
258
+ expect(bead.priority).toBe(0);
259
+ });
260
+
261
+ it("updates multiple fields at once", async () => {
262
+ const result = await beads_update.execute(
263
+ {
264
+ id: testBeadId,
265
+ status: "blocked",
266
+ description: "Blocked on dependency",
267
+ priority: 1,
268
+ },
269
+ mockContext,
270
+ );
271
+
272
+ const bead = parseResponse<Bead>(result);
273
+ expect(bead.status).toBe("blocked");
274
+ expect(bead.description).toContain("Blocked on dependency");
275
+ expect(bead.priority).toBe(1);
276
+ });
277
+
278
+ it("throws BeadError for invalid bead ID", async () => {
279
+ await expect(
280
+ beads_update.execute(
281
+ { id: "nonexistent-bead-xyz", status: "closed" },
282
+ mockContext,
283
+ ),
284
+ ).rejects.toThrow(BeadError);
285
+ });
286
+ });
287
+
288
+ describe("beads_close", () => {
289
+ it("closes a bead with reason", async () => {
290
+ // Create a fresh bead to close
291
+ const createResult = await beads_create.execute(
292
+ { title: "Bead to close" },
293
+ mockContext,
294
+ );
295
+ const created = parseResponse<Bead>(createResult);
296
+ // Don't add to cleanup since we're closing it
297
+
298
+ const result = await beads_close.execute(
299
+ { id: created.id, reason: "Task completed successfully" },
300
+ mockContext,
301
+ );
302
+
303
+ expect(result).toContain("Closed");
304
+ expect(result).toContain(created.id);
305
+
306
+ // Verify it's actually closed using bd show (query has limit issues with many closed beads)
307
+ const showResult = await Bun.$`bd show ${created.id} --json`.quiet();
308
+ const showData = JSON.parse(showResult.stdout.toString());
309
+ const closedBead = Array.isArray(showData) ? showData[0] : showData;
310
+ expect(closedBead.status).toBe("closed");
311
+ });
312
+
313
+ it("throws BeadError for invalid bead ID", async () => {
314
+ await expect(
315
+ beads_close.execute(
316
+ { id: "nonexistent-bead-xyz", reason: "Test" },
317
+ mockContext,
318
+ ),
319
+ ).rejects.toThrow(BeadError);
320
+ });
321
+ });
322
+
323
+ describe("beads_start", () => {
324
+ it("marks a bead as in_progress", async () => {
325
+ // Create a fresh bead
326
+ const createResult = await beads_create.execute(
327
+ { title: "Bead to start" },
328
+ mockContext,
329
+ );
330
+ const created = parseResponse<Bead>(createResult);
331
+ createdBeadIds.push(created.id);
332
+
333
+ expect(created.status).toBe("open");
334
+
335
+ const result = await beads_start.execute({ id: created.id }, mockContext);
336
+
337
+ expect(result).toContain("Started");
338
+ expect(result).toContain(created.id);
339
+
340
+ // Verify status changed
341
+ const queryResult = await beads_query.execute(
342
+ { status: "in_progress" },
343
+ mockContext,
344
+ );
345
+ const inProgressBeads = parseResponse<Bead[]>(queryResult);
346
+ const startedBead = inProgressBeads.find((b) => b.id === created.id);
347
+ expect(startedBead).toBeDefined();
348
+ expect(startedBead?.status).toBe("in_progress");
349
+ });
350
+
351
+ it("throws BeadError for invalid bead ID", async () => {
352
+ await expect(
353
+ beads_start.execute({ id: "nonexistent-bead-xyz" }, mockContext),
354
+ ).rejects.toThrow(BeadError);
355
+ });
356
+ });
357
+
358
+ describe("beads_ready", () => {
359
+ it("returns the highest priority unblocked bead", async () => {
360
+ // Create a high priority bead
361
+ const createResult = await beads_create.execute(
362
+ { title: "High priority ready bead", priority: 0 },
363
+ mockContext,
364
+ );
365
+ const created = parseResponse<Bead>(createResult);
366
+ createdBeadIds.push(created.id);
367
+
368
+ const result = await beads_ready.execute({}, mockContext);
369
+
370
+ // Should return a bead (or "No ready beads" message)
371
+ if (result !== "No ready beads") {
372
+ const bead = parseResponse<Bead>(result);
373
+ expect(bead.id).toBeDefined();
374
+ expect(bead.status).not.toBe("closed");
375
+ expect(bead.status).not.toBe("blocked");
376
+ }
377
+ });
378
+
379
+ it("returns no ready beads message when all are closed", async () => {
380
+ // This test depends on the state of the beads database
381
+ // It may return a bead if there are open ones
382
+ const result = await beads_ready.execute({}, mockContext);
383
+
384
+ expect(typeof result).toBe("string");
385
+ // Either a JSON bead or "No ready beads"
386
+ if (result === "No ready beads") {
387
+ expect(result).toBe("No ready beads");
388
+ } else {
389
+ const bead = parseResponse<Bead>(result);
390
+ expect(bead.id).toBeDefined();
391
+ }
392
+ });
393
+ });
394
+
395
+ describe("beads_create_epic", () => {
396
+ it("creates an epic with subtasks", async () => {
397
+ const result = await beads_create_epic.execute(
398
+ {
399
+ epic_title: "Integration test epic",
400
+ epic_description: "Testing epic creation",
401
+ subtasks: [
402
+ { title: "Subtask 1", priority: 2 },
403
+ { title: "Subtask 2", priority: 3 },
404
+ { title: "Subtask 3", priority: 1 },
405
+ ],
406
+ },
407
+ mockContext,
408
+ );
409
+
410
+ const epicResult = parseResponse<EpicCreateResult>(result);
411
+ createdBeadIds.push(epicResult.epic.id);
412
+ for (const subtask of epicResult.subtasks) {
413
+ createdBeadIds.push(subtask.id);
414
+ }
415
+
416
+ expect(epicResult.success).toBe(true);
417
+ expect(epicResult.epic.title).toBe("Integration test epic");
418
+ expect(epicResult.epic.issue_type).toBe("epic");
419
+ expect(epicResult.subtasks).toHaveLength(3);
420
+
421
+ // Subtasks should have IDs that indicate parent relationship
422
+ // Format: {epic_id}.{index} e.g., "opencode-swarm-plugin-abc.1"
423
+ for (const subtask of epicResult.subtasks) {
424
+ expect(subtask.id).toContain(epicResult.epic.id);
425
+ expect(subtask.id).toMatch(/\.\d+$/); // ends with .N
426
+ }
427
+ });
428
+
429
+ it("creates an epic with files metadata in subtasks", async () => {
430
+ const result = await beads_create_epic.execute(
431
+ {
432
+ epic_title: "Epic with file references",
433
+ subtasks: [
434
+ { title: "Edit src/a.ts", priority: 2, files: ["src/a.ts"] },
435
+ {
436
+ title: "Edit src/b.ts",
437
+ priority: 2,
438
+ files: ["src/b.ts", "src/c.ts"],
439
+ },
440
+ ],
441
+ },
442
+ mockContext,
443
+ );
444
+
445
+ const epicResult = parseResponse<EpicCreateResult>(result);
446
+ createdBeadIds.push(epicResult.epic.id);
447
+ for (const subtask of epicResult.subtasks) {
448
+ createdBeadIds.push(subtask.id);
449
+ }
450
+
451
+ expect(epicResult.success).toBe(true);
452
+ expect(epicResult.subtasks).toHaveLength(2);
453
+ });
454
+
455
+ it("creates epic with single subtask", async () => {
456
+ const result = await beads_create_epic.execute(
457
+ {
458
+ epic_title: "Single subtask epic",
459
+ subtasks: [{ title: "Only task", priority: 1 }],
460
+ },
461
+ mockContext,
462
+ );
463
+
464
+ const epicResult = parseResponse<EpicCreateResult>(result);
465
+ createdBeadIds.push(epicResult.epic.id);
466
+ createdBeadIds.push(epicResult.subtasks[0].id);
467
+
468
+ expect(epicResult.success).toBe(true);
469
+ expect(epicResult.subtasks).toHaveLength(1);
470
+ });
471
+
472
+ it("preserves subtask order", async () => {
473
+ const titles = ["First", "Second", "Third", "Fourth"];
474
+ const result = await beads_create_epic.execute(
475
+ {
476
+ epic_title: "Ordered subtasks epic",
477
+ subtasks: titles.map((title, i) => ({ title, priority: 2 })),
478
+ },
479
+ mockContext,
480
+ );
481
+
482
+ const epicResult = parseResponse<EpicCreateResult>(result);
483
+ createdBeadIds.push(epicResult.epic.id);
484
+ for (const subtask of epicResult.subtasks) {
485
+ createdBeadIds.push(subtask.id);
486
+ }
487
+
488
+ expect(epicResult.success).toBe(true);
489
+ // Subtasks should be in creation order
490
+ for (let i = 0; i < titles.length; i++) {
491
+ expect(epicResult.subtasks[i].title).toBe(titles[i]);
492
+ }
493
+ });
494
+ });
495
+
496
+ describe("beads_link_thread", () => {
497
+ let testBeadId: string;
498
+
499
+ beforeEach(async () => {
500
+ const result = await beads_create.execute(
501
+ { title: "Thread link test bead" },
502
+ mockContext,
503
+ );
504
+ const bead = parseResponse<Bead>(result);
505
+ testBeadId = bead.id;
506
+ createdBeadIds.push(testBeadId);
507
+ });
508
+
509
+ it("links a bead to an Agent Mail thread", async () => {
510
+ const threadId = "test-thread-123";
511
+ const result = await beads_link_thread.execute(
512
+ { bead_id: testBeadId, thread_id: threadId },
513
+ mockContext,
514
+ );
515
+
516
+ expect(result).toContain("Linked");
517
+ expect(result).toContain(testBeadId);
518
+ expect(result).toContain(threadId);
519
+
520
+ // Verify the thread marker is in the description using bd show
521
+ const showResult = await Bun.$`bd show ${testBeadId} --json`.quiet();
522
+ const showData = JSON.parse(showResult.stdout.toString());
523
+ const linkedBead = Array.isArray(showData) ? showData[0] : showData;
524
+ expect(linkedBead.description).toContain(`[thread:${threadId}]`);
525
+ });
526
+
527
+ it("returns message if thread already linked", async () => {
528
+ const threadId = "test-thread-456";
529
+
530
+ // Link once
531
+ await beads_link_thread.execute(
532
+ { bead_id: testBeadId, thread_id: threadId },
533
+ mockContext,
534
+ );
535
+
536
+ // Try to link again
537
+ const result = await beads_link_thread.execute(
538
+ { bead_id: testBeadId, thread_id: threadId },
539
+ mockContext,
540
+ );
541
+
542
+ expect(result).toContain("already linked");
543
+ });
544
+
545
+ it("preserves existing description when linking", async () => {
546
+ // Update bead with a description first
547
+ await beads_update.execute(
548
+ { id: testBeadId, description: "Important context here" },
549
+ mockContext,
550
+ );
551
+
552
+ const threadId = "test-thread-789";
553
+ await beads_link_thread.execute(
554
+ { bead_id: testBeadId, thread_id: threadId },
555
+ mockContext,
556
+ );
557
+
558
+ // Verify both original description and thread marker exist using bd show
559
+ const showResult = await Bun.$`bd show ${testBeadId} --json`.quiet();
560
+ const showData = JSON.parse(showResult.stdout.toString());
561
+ const linkedBead = Array.isArray(showData) ? showData[0] : showData;
562
+
563
+ expect(linkedBead.description).toContain("Important context here");
564
+ expect(linkedBead.description).toContain(`[thread:${threadId}]`);
565
+ });
566
+
567
+ it("throws BeadError for invalid bead ID", async () => {
568
+ await expect(
569
+ beads_link_thread.execute(
570
+ { bead_id: "nonexistent-bead-xyz", thread_id: "thread-123" },
571
+ mockContext,
572
+ ),
573
+ ).rejects.toThrow(BeadError);
574
+ });
575
+ });
576
+
577
+ describe("error handling", () => {
578
+ it("throws BeadError with command info on CLI failure", async () => {
579
+ try {
580
+ await beads_update.execute(
581
+ { id: "definitely-not-a-real-bead-id", status: "closed" },
582
+ mockContext,
583
+ );
584
+ expect.fail("Should have thrown");
585
+ } catch (error) {
586
+ expect(error).toBeInstanceOf(BeadError);
587
+ const beadError = error as BeadError;
588
+ expect(beadError.command).toContain("bd");
589
+ expect(beadError.exitCode).toBeDefined();
590
+ }
591
+ });
592
+ });
593
+
594
+ describe("workflow integration", () => {
595
+ it("complete bead lifecycle: create -> start -> update -> close", async () => {
596
+ // 1. Create
597
+ const createResult = await beads_create.execute(
598
+ { title: "Lifecycle test bead", type: "task", priority: 2 },
599
+ mockContext,
600
+ );
601
+ const bead = parseResponse<Bead>(createResult);
602
+ expect(bead.status).toBe("open");
603
+
604
+ // 2. Start (in_progress)
605
+ const startResult = await beads_start.execute(
606
+ { id: bead.id },
607
+ mockContext,
608
+ );
609
+ expect(startResult).toContain("Started");
610
+
611
+ // 3. Update (add progress note)
612
+ const updateResult = await beads_update.execute(
613
+ { id: bead.id, description: "50% complete" },
614
+ mockContext,
615
+ );
616
+ const updated = parseResponse<Bead>(updateResult);
617
+ expect(updated.description).toContain("50%");
618
+
619
+ // 4. Close
620
+ const closeResult = await beads_close.execute(
621
+ { id: bead.id, reason: "Completed successfully" },
622
+ mockContext,
623
+ );
624
+ expect(closeResult).toContain("Closed");
625
+
626
+ // Verify final state using bd show
627
+ const showResult = await Bun.$`bd show ${bead.id} --json`.quiet();
628
+ const showData = JSON.parse(showResult.stdout.toString());
629
+ const finalBead = Array.isArray(showData) ? showData[0] : showData;
630
+ expect(finalBead.status).toBe("closed");
631
+ });
632
+
633
+ it("epic workflow: create epic -> start subtasks -> close subtasks -> close epic", async () => {
634
+ // 1. Create epic with subtasks
635
+ const epicResult = await beads_create_epic.execute(
636
+ {
637
+ epic_title: "Workflow test epic",
638
+ subtasks: [
639
+ { title: "Step 1", priority: 2 },
640
+ { title: "Step 2", priority: 2 },
641
+ ],
642
+ },
643
+ mockContext,
644
+ );
645
+ const epic = parseResponse<EpicCreateResult>(epicResult);
646
+ expect(epic.success).toBe(true);
647
+
648
+ // 2. Start and complete first subtask
649
+ await beads_start.execute({ id: epic.subtasks[0].id }, mockContext);
650
+ await beads_close.execute(
651
+ { id: epic.subtasks[0].id, reason: "Step 1 done" },
652
+ mockContext,
653
+ );
654
+
655
+ // 3. Start and complete second subtask
656
+ await beads_start.execute({ id: epic.subtasks[1].id }, mockContext);
657
+ await beads_close.execute(
658
+ { id: epic.subtasks[1].id, reason: "Step 2 done" },
659
+ mockContext,
660
+ );
661
+
662
+ // 4. Close the epic
663
+ await beads_close.execute(
664
+ { id: epic.epic.id, reason: "All subtasks completed" },
665
+ mockContext,
666
+ );
667
+
668
+ // Verify all are closed using bd show
669
+ const epicShowResult =
670
+ await Bun.$`bd show ${epic.epic.id} --json`.quiet();
671
+ const epicShowData = JSON.parse(epicShowResult.stdout.toString());
672
+ const epicClosed = Array.isArray(epicShowData)
673
+ ? epicShowData[0]
674
+ : epicShowData;
675
+ expect(epicClosed.status).toBe("closed");
676
+
677
+ for (const subtask of epic.subtasks) {
678
+ const subtaskShowResult =
679
+ await Bun.$`bd show ${subtask.id} --json`.quiet();
680
+ const subtaskShowData = JSON.parse(subtaskShowResult.stdout.toString());
681
+ const subtaskClosed = Array.isArray(subtaskShowData)
682
+ ? subtaskShowData[0]
683
+ : subtaskShowData;
684
+ expect(subtaskClosed.status).toBe("closed");
685
+ }
686
+ });
687
+ });
688
+ });