beth-copilot 1.0.18 → 1.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.
Files changed (291) hide show
  1. package/CHANGELOG.md +41 -28
  2. package/README.md +87 -247
  3. package/bin/cli.js +158 -358
  4. package/dist/__tests__/smoke.test.d.ts +8 -0
  5. package/dist/__tests__/smoke.test.d.ts.map +1 -0
  6. package/dist/__tests__/smoke.test.js +49 -0
  7. package/dist/__tests__/smoke.test.js.map +1 -0
  8. package/dist/cli/commands/beads.e2e.test.d.ts +13 -0
  9. package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -0
  10. package/dist/cli/commands/beads.e2e.test.js +526 -0
  11. package/dist/cli/commands/beads.e2e.test.js.map +1 -0
  12. package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts +32 -0
  13. package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts.map +1 -0
  14. package/dist/cli/commands/cli-edge-cases.e2e.test.js +162 -0
  15. package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -0
  16. package/dist/cli/commands/close.d.ts +89 -0
  17. package/dist/cli/commands/close.d.ts.map +1 -0
  18. package/dist/cli/commands/close.e2e.test.d.ts +27 -0
  19. package/dist/cli/commands/close.e2e.test.d.ts.map +1 -0
  20. package/dist/cli/commands/close.e2e.test.js +252 -0
  21. package/dist/cli/commands/close.e2e.test.js.map +1 -0
  22. package/dist/cli/commands/close.js +309 -0
  23. package/dist/cli/commands/close.js.map +1 -0
  24. package/dist/cli/commands/close.test.d.ts +15 -0
  25. package/dist/cli/commands/close.test.d.ts.map +1 -0
  26. package/dist/cli/commands/close.test.js +634 -0
  27. package/dist/cli/commands/close.test.js.map +1 -0
  28. package/dist/cli/commands/doctor.d.ts +23 -0
  29. package/dist/cli/commands/doctor.d.ts.map +1 -1
  30. package/dist/cli/commands/doctor.js +93 -0
  31. package/dist/cli/commands/doctor.js.map +1 -1
  32. package/dist/cli/commands/doctor.test.js +209 -0
  33. package/dist/cli/commands/doctor.test.js.map +1 -1
  34. package/dist/cli/commands/framework-isolation.test.d.ts +30 -0
  35. package/dist/cli/commands/framework-isolation.test.d.ts.map +1 -0
  36. package/dist/cli/commands/framework-isolation.test.js +119 -0
  37. package/dist/cli/commands/framework-isolation.test.js.map +1 -0
  38. package/dist/cli/commands/help.e2e.test.js +4 -4
  39. package/dist/cli/commands/help.e2e.test.js.map +1 -1
  40. package/dist/cli/commands/init-logic.e2e.test.d.ts +37 -0
  41. package/dist/cli/commands/init-logic.e2e.test.d.ts.map +1 -0
  42. package/dist/cli/commands/init-logic.e2e.test.js +305 -0
  43. package/dist/cli/commands/init-logic.e2e.test.js.map +1 -0
  44. package/dist/cli/commands/land.d.ts +142 -0
  45. package/dist/cli/commands/land.d.ts.map +1 -0
  46. package/dist/cli/commands/land.js +647 -0
  47. package/dist/cli/commands/land.js.map +1 -0
  48. package/dist/cli/commands/land.test.d.ts +20 -0
  49. package/dist/cli/commands/land.test.d.ts.map +1 -0
  50. package/dist/cli/commands/land.test.js +622 -0
  51. package/dist/cli/commands/land.test.js.map +1 -0
  52. package/dist/cli/commands/mcp.e2e.test.js +22 -29
  53. package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
  54. package/dist/cli/commands/pipeline.e2e.test.js +20 -20
  55. package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
  56. package/dist/cli/commands/pre-push-guard.d.ts +84 -0
  57. package/dist/cli/commands/pre-push-guard.d.ts.map +1 -0
  58. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts +24 -0
  59. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts.map +1 -0
  60. package/dist/cli/commands/pre-push-guard.e2e.test.js +171 -0
  61. package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -0
  62. package/dist/cli/commands/pre-push-guard.js +257 -0
  63. package/dist/cli/commands/pre-push-guard.js.map +1 -0
  64. package/dist/cli/commands/pre-push-guard.test.d.ts +15 -0
  65. package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -0
  66. package/dist/cli/commands/pre-push-guard.test.js +397 -0
  67. package/dist/cli/commands/pre-push-guard.test.js.map +1 -0
  68. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +23 -0
  69. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts.map +1 -0
  70. package/dist/cli/commands/quickstart-expanded.e2e.test.js +179 -0
  71. package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -0
  72. package/dist/cli/commands/quickstart.d.ts.map +1 -1
  73. package/dist/cli/commands/quickstart.js +7 -23
  74. package/dist/cli/commands/quickstart.js.map +1 -1
  75. package/dist/cli/commands/quickstart.test.js +40 -67
  76. package/dist/cli/commands/quickstart.test.js.map +1 -1
  77. package/dist/core/agents/suite.test.js +4 -2
  78. package/dist/core/agents/suite.test.js.map +1 -1
  79. package/dist/core/agents/tools.test.js +5 -1
  80. package/dist/core/agents/tools.test.js.map +1 -1
  81. package/dist/index.d.ts +3 -10
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +5 -10
  84. package/dist/index.js.map +1 -1
  85. package/package.json +15 -9
  86. package/sbom.json +2011 -819
  87. package/templates/.github/agents/beth.agent.md +220 -66
  88. package/templates/.github/agents/developer.agent.md +53 -90
  89. package/templates/.github/agents/product-manager.agent.md +15 -68
  90. package/templates/.github/agents/researcher.agent.md +20 -71
  91. package/templates/.github/agents/security-reviewer.agent.md +29 -81
  92. package/templates/.github/agents/tester.agent.md +40 -69
  93. package/templates/.github/agents/ux-designer.agent.md +20 -74
  94. package/templates/.github/copilot-instructions.md +217 -225
  95. package/templates/AGENTS.md +108 -20
  96. package/templates/mcp.json.example +0 -3
  97. package/dist/cli/commands/client-config.d.ts +0 -31
  98. package/dist/cli/commands/client-config.d.ts.map +0 -1
  99. package/dist/cli/commands/client-config.e2e.test.d.ts +0 -15
  100. package/dist/cli/commands/client-config.e2e.test.d.ts.map +0 -1
  101. package/dist/cli/commands/client-config.e2e.test.js +0 -556
  102. package/dist/cli/commands/client-config.e2e.test.js.map +0 -1
  103. package/dist/cli/commands/client-config.js +0 -73
  104. package/dist/cli/commands/client-config.js.map +0 -1
  105. package/dist/cli/commands/client-config.test.d.ts +0 -6
  106. package/dist/cli/commands/client-config.test.d.ts.map +0 -1
  107. package/dist/cli/commands/client-config.test.js +0 -133
  108. package/dist/cli/commands/client-config.test.js.map +0 -1
  109. package/dist/cli/commands/init-quickstart.e2e.test.d.ts +0 -11
  110. package/dist/cli/commands/init-quickstart.e2e.test.d.ts.map +0 -1
  111. package/dist/cli/commands/init-quickstart.e2e.test.js +0 -221
  112. package/dist/cli/commands/init-quickstart.e2e.test.js.map +0 -1
  113. package/dist/core/context.d.ts +0 -171
  114. package/dist/core/context.d.ts.map +0 -1
  115. package/dist/core/context.js +0 -353
  116. package/dist/core/context.js.map +0 -1
  117. package/dist/core/context.test.d.ts +0 -8
  118. package/dist/core/context.test.d.ts.map +0 -1
  119. package/dist/core/context.test.js +0 -253
  120. package/dist/core/context.test.js.map +0 -1
  121. package/dist/core/handoffs.d.ts +0 -151
  122. package/dist/core/handoffs.d.ts.map +0 -1
  123. package/dist/core/handoffs.js +0 -220
  124. package/dist/core/handoffs.js.map +0 -1
  125. package/dist/core/handoffs.test.d.ts +0 -8
  126. package/dist/core/handoffs.test.d.ts.map +0 -1
  127. package/dist/core/handoffs.test.js +0 -231
  128. package/dist/core/handoffs.test.js.map +0 -1
  129. package/dist/core/orchestrator.d.ts +0 -246
  130. package/dist/core/orchestrator.d.ts.map +0 -1
  131. package/dist/core/orchestrator.js +0 -514
  132. package/dist/core/orchestrator.js.map +0 -1
  133. package/dist/core/orchestrator.test.d.ts +0 -8
  134. package/dist/core/orchestrator.test.d.ts.map +0 -1
  135. package/dist/core/orchestrator.test.js +0 -517
  136. package/dist/core/orchestrator.test.js.map +0 -1
  137. package/dist/core/router.d.ts +0 -102
  138. package/dist/core/router.d.ts.map +0 -1
  139. package/dist/core/router.js +0 -178
  140. package/dist/core/router.js.map +0 -1
  141. package/dist/core/router.test.d.ts +0 -8
  142. package/dist/core/router.test.d.ts.map +0 -1
  143. package/dist/core/router.test.js +0 -215
  144. package/dist/core/router.test.js.map +0 -1
  145. package/dist/init.test.js +0 -288
  146. package/dist/providers/azure.d.ts +0 -147
  147. package/dist/providers/azure.d.ts.map +0 -1
  148. package/dist/providers/azure.js +0 -491
  149. package/dist/providers/azure.js.map +0 -1
  150. package/dist/providers/azure.test.d.ts +0 -11
  151. package/dist/providers/azure.test.d.ts.map +0 -1
  152. package/dist/providers/azure.test.js +0 -330
  153. package/dist/providers/azure.test.js.map +0 -1
  154. package/dist/providers/config.d.ts +0 -87
  155. package/dist/providers/config.d.ts.map +0 -1
  156. package/dist/providers/config.js +0 -193
  157. package/dist/providers/config.js.map +0 -1
  158. package/dist/providers/config.test.d.ts +0 -7
  159. package/dist/providers/config.test.d.ts.map +0 -1
  160. package/dist/providers/config.test.js +0 -370
  161. package/dist/providers/config.test.js.map +0 -1
  162. package/dist/providers/index.d.ts +0 -18
  163. package/dist/providers/index.d.ts.map +0 -1
  164. package/dist/providers/index.js +0 -14
  165. package/dist/providers/index.js.map +0 -1
  166. package/dist/providers/interface.d.ts +0 -191
  167. package/dist/providers/interface.d.ts.map +0 -1
  168. package/dist/providers/interface.js +0 -94
  169. package/dist/providers/interface.js.map +0 -1
  170. package/dist/providers/retry.d.ts +0 -128
  171. package/dist/providers/retry.d.ts.map +0 -1
  172. package/dist/providers/retry.js +0 -205
  173. package/dist/providers/retry.js.map +0 -1
  174. package/dist/providers/retry.test.d.ts +0 -7
  175. package/dist/providers/retry.test.d.ts.map +0 -1
  176. package/dist/providers/retry.test.js +0 -439
  177. package/dist/providers/retry.test.js.map +0 -1
  178. package/dist/providers/streaming.d.ts +0 -157
  179. package/dist/providers/streaming.d.ts.map +0 -1
  180. package/dist/providers/streaming.js +0 -233
  181. package/dist/providers/streaming.js.map +0 -1
  182. package/dist/providers/streaming.test.d.ts +0 -7
  183. package/dist/providers/streaming.test.d.ts.map +0 -1
  184. package/dist/providers/streaming.test.js +0 -372
  185. package/dist/providers/streaming.test.js.map +0 -1
  186. package/dist/providers/types.d.ts +0 -209
  187. package/dist/providers/types.d.ts.map +0 -1
  188. package/dist/providers/types.js +0 -53
  189. package/dist/providers/types.js.map +0 -1
  190. package/dist/providers/types.test.d.ts +0 -7
  191. package/dist/providers/types.test.d.ts.map +0 -1
  192. package/dist/providers/types.test.js +0 -141
  193. package/dist/providers/types.test.js.map +0 -1
  194. package/dist/tools/cli/beads.d.ts +0 -27
  195. package/dist/tools/cli/beads.d.ts.map +0 -1
  196. package/dist/tools/cli/beads.js +0 -172
  197. package/dist/tools/cli/beads.js.map +0 -1
  198. package/dist/tools/cli/beads.test.d.ts +0 -8
  199. package/dist/tools/cli/beads.test.d.ts.map +0 -1
  200. package/dist/tools/cli/beads.test.js +0 -264
  201. package/dist/tools/cli/beads.test.js.map +0 -1
  202. package/dist/tools/cli/editFile.d.ts +0 -17
  203. package/dist/tools/cli/editFile.d.ts.map +0 -1
  204. package/dist/tools/cli/editFile.js +0 -125
  205. package/dist/tools/cli/editFile.js.map +0 -1
  206. package/dist/tools/cli/editFile.test.d.ts +0 -8
  207. package/dist/tools/cli/editFile.test.d.ts.map +0 -1
  208. package/dist/tools/cli/editFile.test.js +0 -177
  209. package/dist/tools/cli/editFile.test.js.map +0 -1
  210. package/dist/tools/cli/readFile.d.ts +0 -25
  211. package/dist/tools/cli/readFile.d.ts.map +0 -1
  212. package/dist/tools/cli/readFile.js +0 -118
  213. package/dist/tools/cli/readFile.js.map +0 -1
  214. package/dist/tools/cli/readFile.test.d.ts +0 -8
  215. package/dist/tools/cli/readFile.test.d.ts.map +0 -1
  216. package/dist/tools/cli/readFile.test.js +0 -194
  217. package/dist/tools/cli/readFile.test.js.map +0 -1
  218. package/dist/tools/cli/search.d.ts +0 -16
  219. package/dist/tools/cli/search.d.ts.map +0 -1
  220. package/dist/tools/cli/search.js +0 -261
  221. package/dist/tools/cli/search.js.map +0 -1
  222. package/dist/tools/cli/search.test.d.ts +0 -8
  223. package/dist/tools/cli/search.test.d.ts.map +0 -1
  224. package/dist/tools/cli/search.test.js +0 -172
  225. package/dist/tools/cli/search.test.js.map +0 -1
  226. package/dist/tools/cli/subagent.d.ts +0 -43
  227. package/dist/tools/cli/subagent.d.ts.map +0 -1
  228. package/dist/tools/cli/subagent.js +0 -99
  229. package/dist/tools/cli/subagent.js.map +0 -1
  230. package/dist/tools/cli/subagent.test.d.ts +0 -8
  231. package/dist/tools/cli/subagent.test.d.ts.map +0 -1
  232. package/dist/tools/cli/subagent.test.js +0 -190
  233. package/dist/tools/cli/subagent.test.js.map +0 -1
  234. package/dist/tools/cli/terminal.d.ts +0 -19
  235. package/dist/tools/cli/terminal.d.ts.map +0 -1
  236. package/dist/tools/cli/terminal.js +0 -164
  237. package/dist/tools/cli/terminal.js.map +0 -1
  238. package/dist/tools/cli/terminal.test.d.ts +0 -8
  239. package/dist/tools/cli/terminal.test.d.ts.map +0 -1
  240. package/dist/tools/cli/terminal.test.js +0 -161
  241. package/dist/tools/cli/terminal.test.js.map +0 -1
  242. package/dist/tools/index.d.ts +0 -25
  243. package/dist/tools/index.d.ts.map +0 -1
  244. package/dist/tools/index.js +0 -41
  245. package/dist/tools/index.js.map +0 -1
  246. package/dist/tools/interface.d.ts +0 -64
  247. package/dist/tools/interface.d.ts.map +0 -1
  248. package/dist/tools/interface.js +0 -37
  249. package/dist/tools/interface.js.map +0 -1
  250. package/dist/tools/interface.test.d.ts +0 -7
  251. package/dist/tools/interface.test.d.ts.map +0 -1
  252. package/dist/tools/interface.test.js +0 -179
  253. package/dist/tools/interface.test.js.map +0 -1
  254. package/dist/tools/mcp/bridge.d.ts +0 -48
  255. package/dist/tools/mcp/bridge.d.ts.map +0 -1
  256. package/dist/tools/mcp/bridge.js +0 -128
  257. package/dist/tools/mcp/bridge.js.map +0 -1
  258. package/dist/tools/mcp/bridge.test.d.ts +0 -8
  259. package/dist/tools/mcp/bridge.test.d.ts.map +0 -1
  260. package/dist/tools/mcp/bridge.test.js +0 -300
  261. package/dist/tools/mcp/bridge.test.js.map +0 -1
  262. package/dist/tools/mcp/client.d.ts +0 -135
  263. package/dist/tools/mcp/client.d.ts.map +0 -1
  264. package/dist/tools/mcp/client.js +0 -263
  265. package/dist/tools/mcp/client.js.map +0 -1
  266. package/dist/tools/mcp/client.test.d.ts +0 -8
  267. package/dist/tools/mcp/client.test.d.ts.map +0 -1
  268. package/dist/tools/mcp/client.test.js +0 -390
  269. package/dist/tools/mcp/client.test.js.map +0 -1
  270. package/dist/tools/registry.d.ts +0 -82
  271. package/dist/tools/registry.d.ts.map +0 -1
  272. package/dist/tools/registry.js +0 -99
  273. package/dist/tools/registry.js.map +0 -1
  274. package/dist/tools/registry.test.d.ts +0 -7
  275. package/dist/tools/registry.test.d.ts.map +0 -1
  276. package/dist/tools/registry.test.js +0 -199
  277. package/dist/tools/registry.test.js.map +0 -1
  278. package/dist/tools/suite.test.d.ts +0 -11
  279. package/dist/tools/suite.test.d.ts.map +0 -1
  280. package/dist/tools/suite.test.js +0 -119
  281. package/dist/tools/suite.test.js.map +0 -1
  282. package/dist/tools/types.d.ts +0 -75
  283. package/dist/tools/types.d.ts.map +0 -1
  284. package/dist/tools/types.js +0 -30
  285. package/dist/tools/types.js.map +0 -1
  286. package/dist/tools/types.test.d.ts +0 -7
  287. package/dist/tools/types.test.d.ts.map +0 -1
  288. package/dist/tools/types.test.js +0 -178
  289. package/dist/tools/types.test.js.map +0 -1
  290. package/templates/.vscode/mcp.json +0 -20
  291. package/templates/CLAUDE.md +0 -129
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Close Command Tests
3
+ *
4
+ * Tests dependency enforcement on bd close:
5
+ * - Issue ID validation
6
+ * - Open children detection
7
+ * - Open blocker detection
8
+ * - Epic test subtask enforcement
9
+ * - Issue type awareness
10
+ * - Arg parsing
11
+ * - Blocked close behavior
12
+ * - Force bypass
13
+ */
14
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
15
+ import * as child_process from 'child_process';
16
+ // Mock child_process before importing the module under test
17
+ vi.mock('child_process', () => ({
18
+ execFileSync: vi.fn(),
19
+ }));
20
+ // Import after mocking
21
+ import { validateIssueId, getOpenChildren, getOpenBlockers, getIssueInfo, getAllChildren, getMissingTestSubtasks, parseCloseArgs, closeIssue, } from './close.js';
22
+ const mockedExecFileSync = vi.mocked(child_process.execFileSync);
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ // Suppress console output in tests
26
+ vi.spyOn(console, 'error').mockImplementation(() => { });
27
+ });
28
+ // ─── validateIssueId ───────────────────────────────────────────────────────────
29
+ describe('validateIssueId', () => {
30
+ it('accepts standard beads IDs', () => {
31
+ expect(validateIssueId('beth-abc')).toBe(true);
32
+ expect(validateIssueId('beth-cip')).toBe(true);
33
+ expect(validateIssueId('beth-abc123')).toBe(true);
34
+ expect(validateIssueId('hq-xyz')).toBe(true);
35
+ });
36
+ it('accepts dotted child IDs', () => {
37
+ expect(validateIssueId('beth-cip.1')).toBe(true);
38
+ expect(validateIssueId('beth-abc123.42')).toBe(true);
39
+ expect(validateIssueId('hq-xyz.9')).toBe(true);
40
+ });
41
+ it('rejects empty string', () => {
42
+ expect(validateIssueId('')).toBe(false);
43
+ });
44
+ it('rejects IDs without rig prefix', () => {
45
+ expect(validateIssueId('abc123')).toBe(false);
46
+ expect(validateIssueId('-abc123')).toBe(false);
47
+ });
48
+ it('rejects IDs with uppercase', () => {
49
+ expect(validateIssueId('BETH-abc')).toBe(false);
50
+ expect(validateIssueId('Beth-abc')).toBe(false);
51
+ });
52
+ it('rejects IDs with special characters', () => {
53
+ expect(validateIssueId('beth-abc; rm -rf /')).toBe(false);
54
+ expect(validateIssueId('beth-abc$(whoami)')).toBe(false);
55
+ expect(validateIssueId('beth-abc`cmd`')).toBe(false);
56
+ expect(validateIssueId('beth-abc|cat /etc/passwd')).toBe(false);
57
+ });
58
+ it('rejects overly long hashes', () => {
59
+ expect(validateIssueId('beth-abcdefghijk')).toBe(false); // 11 chars
60
+ });
61
+ it('rejects double dots', () => {
62
+ expect(validateIssueId('beth-abc.1.2')).toBe(false);
63
+ });
64
+ it('rejects dot without number', () => {
65
+ expect(validateIssueId('beth-abc.')).toBe(false);
66
+ expect(validateIssueId('beth-abc.x')).toBe(false);
67
+ });
68
+ });
69
+ // ─── getOpenChildren ────────────────────────────────────────────────────────────
70
+ describe('getOpenChildren', () => {
71
+ it('returns open children from bd children --json', () => {
72
+ const mockChildren = [
73
+ { id: 'beth-abc.1', title: 'Child 1', status: 'open' },
74
+ { id: 'beth-abc.2', title: 'Child 2', status: 'open' },
75
+ ];
76
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockChildren));
77
+ const result = getOpenChildren('beth-abc');
78
+ expect(result).toHaveLength(2);
79
+ expect(result[0].id).toBe('beth-abc.1');
80
+ expect(mockedExecFileSync).toHaveBeenCalledWith('bd', ['children', 'beth-abc', '--json'], expect.objectContaining({ encoding: 'utf-8' }));
81
+ });
82
+ it('filters out closed children', () => {
83
+ const mockChildren = [
84
+ { id: 'beth-abc.1', title: 'Done', status: 'closed' },
85
+ { id: 'beth-abc.2', title: 'Still open', status: 'open' },
86
+ ];
87
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockChildren));
88
+ const result = getOpenChildren('beth-abc');
89
+ expect(result).toHaveLength(1);
90
+ expect(result[0].id).toBe('beth-abc.2');
91
+ });
92
+ it('returns empty array when no children', () => {
93
+ mockedExecFileSync.mockReturnValue('[]');
94
+ const result = getOpenChildren('beth-abc');
95
+ expect(result).toHaveLength(0);
96
+ });
97
+ it('returns empty array when bd not available', () => {
98
+ mockedExecFileSync.mockImplementation(() => {
99
+ throw new Error('bd not found');
100
+ });
101
+ const result = getOpenChildren('beth-abc');
102
+ expect(result).toHaveLength(0);
103
+ });
104
+ it('returns empty array on invalid JSON', () => {
105
+ mockedExecFileSync.mockReturnValue('not json');
106
+ const result = getOpenChildren('beth-abc');
107
+ expect(result).toHaveLength(0);
108
+ });
109
+ it('returns empty array when response is not an array', () => {
110
+ mockedExecFileSync.mockReturnValue('{"error": "not found"}');
111
+ const result = getOpenChildren('beth-abc');
112
+ expect(result).toHaveLength(0);
113
+ });
114
+ it('filters out malformed child entries', () => {
115
+ const mockChildren = [
116
+ { id: 'beth-abc.1', title: 'Valid', status: 'open' },
117
+ { id: 'beth-abc.2' }, // missing title and status
118
+ null,
119
+ 'garbage',
120
+ ];
121
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockChildren));
122
+ const result = getOpenChildren('beth-abc');
123
+ expect(result).toHaveLength(1);
124
+ expect(result[0].id).toBe('beth-abc.1');
125
+ });
126
+ });
127
+ // ─── getOpenBlockers ────────────────────────────────────────────────────────────
128
+ describe('getOpenBlockers', () => {
129
+ it('returns open non-parent-child blockers', () => {
130
+ const mockDeps = [
131
+ { id: 'beth-xyz', title: 'Blocker', status: 'open', dependency_type: 'blocks' },
132
+ { id: 'beth-abc', title: 'Parent', status: 'open', dependency_type: 'parent-child' },
133
+ ];
134
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockDeps));
135
+ const result = getOpenBlockers('beth-abc.1');
136
+ expect(result).toHaveLength(1);
137
+ expect(result[0].id).toBe('beth-xyz');
138
+ expect(result[0].dependency_type).toBe('blocks');
139
+ });
140
+ it('filters out closed blockers', () => {
141
+ const mockDeps = [
142
+ { id: 'beth-xyz', title: 'Resolved', status: 'closed', dependency_type: 'blocks' },
143
+ ];
144
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockDeps));
145
+ const result = getOpenBlockers('beth-abc.1');
146
+ expect(result).toHaveLength(0);
147
+ });
148
+ it('excludes parent-child dependencies', () => {
149
+ const mockDeps = [
150
+ { id: 'beth-abc', title: 'Parent', status: 'open', dependency_type: 'parent-child' },
151
+ ];
152
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockDeps));
153
+ const result = getOpenBlockers('beth-abc.1');
154
+ expect(result).toHaveLength(0);
155
+ });
156
+ it('returns empty array when bd not available', () => {
157
+ mockedExecFileSync.mockImplementation(() => {
158
+ throw new Error('bd not found');
159
+ });
160
+ const result = getOpenBlockers('beth-abc.1');
161
+ expect(result).toHaveLength(0);
162
+ });
163
+ it('returns empty array on invalid JSON', () => {
164
+ mockedExecFileSync.mockReturnValue('not json');
165
+ const result = getOpenBlockers('beth-abc.1');
166
+ expect(result).toHaveLength(0);
167
+ });
168
+ it('handles multiple open blockers', () => {
169
+ const mockDeps = [
170
+ { id: 'beth-aaa', title: 'Blocker A', status: 'open', dependency_type: 'blocks' },
171
+ { id: 'beth-bbb', title: 'Blocker B', status: 'in_progress', dependency_type: 'blocks' },
172
+ ];
173
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockDeps));
174
+ const result = getOpenBlockers('beth-abc.1');
175
+ expect(result).toHaveLength(2);
176
+ });
177
+ it('calls bd dep list with correct args', () => {
178
+ mockedExecFileSync.mockReturnValue('[]');
179
+ getOpenBlockers('beth-abc.1');
180
+ expect(mockedExecFileSync).toHaveBeenCalledWith('bd', ['dep', 'list', 'beth-abc.1', '--json'], expect.objectContaining({ encoding: 'utf-8' }));
181
+ });
182
+ });
183
+ // ─── getIssueInfo ───────────────────────────────────────────────────────────────
184
+ describe('getIssueInfo', () => {
185
+ it('returns issue metadata from bd show --json', () => {
186
+ const mockIssue = [
187
+ { id: 'beth-abc', title: 'Feature', status: 'open', issue_type: 'epic' },
188
+ ];
189
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockIssue));
190
+ const result = getIssueInfo('beth-abc');
191
+ expect(result).not.toBeNull();
192
+ expect(result.issue_type).toBe('epic');
193
+ expect(result.id).toBe('beth-abc');
194
+ });
195
+ it('returns issue metadata for non-epic tasks', () => {
196
+ const mockIssue = [
197
+ { id: 'beth-abc.1', title: 'Task', status: 'open', issue_type: 'task' },
198
+ ];
199
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockIssue));
200
+ const result = getIssueInfo('beth-abc.1');
201
+ expect(result).not.toBeNull();
202
+ expect(result.issue_type).toBe('task');
203
+ });
204
+ it('returns null when bd not available', () => {
205
+ mockedExecFileSync.mockImplementation(() => {
206
+ throw new Error('bd not found');
207
+ });
208
+ const result = getIssueInfo('beth-abc');
209
+ expect(result).toBeNull();
210
+ });
211
+ it('returns null on empty array', () => {
212
+ mockedExecFileSync.mockReturnValue('[]');
213
+ const result = getIssueInfo('beth-abc');
214
+ expect(result).toBeNull();
215
+ });
216
+ it('returns null when missing issue_type', () => {
217
+ const mockIssue = [{ id: 'beth-abc', title: 'Incomplete' }];
218
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockIssue));
219
+ const result = getIssueInfo('beth-abc');
220
+ expect(result).toBeNull();
221
+ });
222
+ });
223
+ // ─── getAllChildren ──────────────────────────────────────────────────────────────
224
+ describe('getAllChildren', () => {
225
+ it('returns all children including closed from dependents', () => {
226
+ const mockShow = [
227
+ {
228
+ id: 'beth-abc',
229
+ issue_type: 'epic',
230
+ dependents: [
231
+ { id: 'beth-abc.1', title: 'Impl', status: 'closed' },
232
+ { id: 'beth-abc.2', title: 'Unit tests for impl', status: 'closed' },
233
+ { id: 'beth-abc.3', title: 'E2E tests for impl', status: 'closed' },
234
+ ],
235
+ },
236
+ ];
237
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockShow));
238
+ const result = getAllChildren('beth-abc');
239
+ expect(result).toHaveLength(3);
240
+ });
241
+ it('returns empty array when no dependents', () => {
242
+ const mockShow = [
243
+ { id: 'beth-abc', issue_type: 'task' },
244
+ ];
245
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockShow));
246
+ const result = getAllChildren('beth-abc');
247
+ expect(result).toHaveLength(0);
248
+ });
249
+ it('returns empty array when bd not available', () => {
250
+ mockedExecFileSync.mockImplementation(() => {
251
+ throw new Error('bd not found');
252
+ });
253
+ const result = getAllChildren('beth-abc');
254
+ expect(result).toHaveLength(0);
255
+ });
256
+ it('filters out malformed dependents', () => {
257
+ const mockShow = [
258
+ {
259
+ id: 'beth-abc',
260
+ dependents: [
261
+ { id: 'beth-abc.1', title: 'Valid', status: 'open' },
262
+ { id: 'beth-abc.2' }, // missing title/status
263
+ null,
264
+ ],
265
+ },
266
+ ];
267
+ mockedExecFileSync.mockReturnValue(JSON.stringify(mockShow));
268
+ const result = getAllChildren('beth-abc');
269
+ expect(result).toHaveLength(1);
270
+ });
271
+ });
272
+ // ─── getMissingTestSubtasks ────────────────────────────────────────────────────
273
+ describe('getMissingTestSubtasks', () => {
274
+ it('returns all three when no test subtasks exist', () => {
275
+ const children = [
276
+ { id: 'beth-abc.1', title: 'Implement feature', status: 'closed' },
277
+ ];
278
+ const missing = getMissingTestSubtasks(children);
279
+ expect(missing).toHaveLength(3);
280
+ expect(missing).toContain('Unit tests');
281
+ expect(missing).toContain('E2E/Integration tests');
282
+ expect(missing).toContain('Security tests');
283
+ });
284
+ it('returns empty when all test subtasks present', () => {
285
+ const children = [
286
+ { id: 'beth-abc.1', title: 'Implement feature', status: 'closed' },
287
+ { id: 'beth-abc.2', title: 'Unit tests for feature', status: 'closed' },
288
+ { id: 'beth-abc.3', title: 'E2E tests for feature', status: 'closed' },
289
+ { id: 'beth-abc.4', title: 'Security tests for feature', status: 'closed' },
290
+ ];
291
+ const missing = getMissingTestSubtasks(children);
292
+ expect(missing).toHaveLength(0);
293
+ });
294
+ it('detects missing unit tests', () => {
295
+ const children = [
296
+ { id: 'beth-abc.1', title: 'E2E tests for auth', status: 'closed' },
297
+ { id: 'beth-abc.2', title: 'Security tests for auth', status: 'closed' },
298
+ ];
299
+ const missing = getMissingTestSubtasks(children);
300
+ expect(missing).toEqual(['Unit tests']);
301
+ });
302
+ it('detects missing e2e tests', () => {
303
+ const children = [
304
+ { id: 'beth-abc.1', title: 'Unit tests for auth', status: 'closed' },
305
+ { id: 'beth-abc.2', title: 'Security tests for auth', status: 'closed' },
306
+ ];
307
+ const missing = getMissingTestSubtasks(children);
308
+ expect(missing).toEqual(['E2E/Integration tests']);
309
+ });
310
+ it('detects missing security tests', () => {
311
+ const children = [
312
+ { id: 'beth-abc.1', title: 'Unit tests for auth', status: 'closed' },
313
+ { id: 'beth-abc.2', title: 'E2E tests for auth', status: 'closed' },
314
+ ];
315
+ const missing = getMissingTestSubtasks(children);
316
+ expect(missing).toEqual(['Security tests']);
317
+ });
318
+ it('matches "integration tests" as e2e', () => {
319
+ const children = [
320
+ { id: 'beth-abc.1', title: 'Integration tests for API', status: 'closed' },
321
+ ];
322
+ const missing = getMissingTestSubtasks(children);
323
+ expect(missing).not.toContain('E2E/Integration tests');
324
+ });
325
+ it('matches "end-to-end tests" as e2e', () => {
326
+ const children = [
327
+ { id: 'beth-abc.1', title: 'End-to-end tests for auth', status: 'closed' },
328
+ ];
329
+ const missing = getMissingTestSubtasks(children);
330
+ expect(missing).not.toContain('E2E/Integration tests');
331
+ });
332
+ it('is case-insensitive', () => {
333
+ const children = [
334
+ { id: 'beth-abc.1', title: 'UNIT TESTS for feature', status: 'closed' },
335
+ { id: 'beth-abc.2', title: 'e2e Tests for feature', status: 'closed' },
336
+ { id: 'beth-abc.3', title: 'Security Tests for feature', status: 'closed' },
337
+ ];
338
+ const missing = getMissingTestSubtasks(children);
339
+ expect(missing).toHaveLength(0);
340
+ });
341
+ it('returns all three for empty children array', () => {
342
+ const missing = getMissingTestSubtasks([]);
343
+ expect(missing).toHaveLength(3);
344
+ });
345
+ });
346
+ // ─── parseCloseArgs ─────────────────────────────────────────────────────────────
347
+ describe('parseCloseArgs', () => {
348
+ it('extracts a single issue ID', () => {
349
+ const { issueIds, reason, force } = parseCloseArgs(['beth-abc']);
350
+ expect(issueIds).toEqual(['beth-abc']);
351
+ expect(reason).toBeUndefined();
352
+ expect(force).toBe(false);
353
+ });
354
+ it('extracts multiple issue IDs', () => {
355
+ const { issueIds } = parseCloseArgs(['beth-abc.1', 'beth-abc.2']);
356
+ expect(issueIds).toEqual(['beth-abc.1', 'beth-abc.2']);
357
+ });
358
+ it('extracts --reason with separate arg', () => {
359
+ const { issueIds, reason } = parseCloseArgs([
360
+ 'beth-abc',
361
+ '--reason',
362
+ 'Task completed',
363
+ ]);
364
+ expect(issueIds).toEqual(['beth-abc']);
365
+ expect(reason).toBe('Task completed');
366
+ });
367
+ it('extracts -r shorthand', () => {
368
+ const { reason } = parseCloseArgs(['beth-abc', '-r', 'Done']);
369
+ expect(reason).toBe('Done');
370
+ });
371
+ it('extracts --reason=value format', () => {
372
+ const { reason } = parseCloseArgs(['beth-abc', '--reason=Completed']);
373
+ expect(reason).toBe('Completed');
374
+ });
375
+ it('extracts --force flag', () => {
376
+ const { force } = parseCloseArgs(['beth-abc', '--force']);
377
+ expect(force).toBe(true);
378
+ });
379
+ it('extracts -f shorthand', () => {
380
+ const { force } = parseCloseArgs(['beth-abc', '-f']);
381
+ expect(force).toBe(true);
382
+ });
383
+ it('handles combination of all args', () => {
384
+ const { issueIds, reason, force } = parseCloseArgs([
385
+ 'beth-abc.1',
386
+ 'beth-abc.2',
387
+ '--reason',
388
+ 'Both done',
389
+ '--force',
390
+ ]);
391
+ expect(issueIds).toEqual(['beth-abc.1', 'beth-abc.2']);
392
+ expect(reason).toBe('Both done');
393
+ expect(force).toBe(true);
394
+ });
395
+ it('returns empty issueIds when no args', () => {
396
+ const { issueIds } = parseCloseArgs([]);
397
+ expect(issueIds).toEqual([]);
398
+ });
399
+ it('skips unknown flags', () => {
400
+ const { issueIds } = parseCloseArgs(['beth-abc', '--json', '--verbose']);
401
+ expect(issueIds).toEqual(['beth-abc']);
402
+ });
403
+ });
404
+ // ─── closeIssue ─────────────────────────────────────────────────────────────────
405
+ //
406
+ // Call order for non-force close:
407
+ // 1. bd dep list <id> --json → getOpenBlockers
408
+ // 2. bd children <id> --json → getOpenChildren
409
+ // 3. bd show <id> --json → getIssueInfo (check if epic)
410
+ // 4. bd show <id> --json → getAllChildren (if epic, for test subtask check)
411
+ // 5. bd close <id> [flags] → actual close
412
+ //
413
+ describe('closeIssue', () => {
414
+ /**
415
+ * Helper: mock the standard "no blockers, no children, non-epic task" path.
416
+ * Returns 4 calls: dep list → children → show (issue info) → close
417
+ */
418
+ function mockCleanLeafTask() {
419
+ // 1. No open blockers
420
+ mockedExecFileSync.mockReturnValueOnce('[]');
421
+ // 2. No open children
422
+ mockedExecFileSync.mockReturnValueOnce('[]');
423
+ // 3. Issue info: task (not epic)
424
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{ id: 'beth-abc', title: 'Task', status: 'open', issue_type: 'task' }]));
425
+ // 4. bd close succeeds
426
+ mockedExecFileSync.mockReturnValueOnce('');
427
+ }
428
+ /**
429
+ * Helper: mock a clean epic close path (no blockers, no open children, has test subtasks).
430
+ */
431
+ function mockCleanEpicWithTests() {
432
+ // 1. No open blockers
433
+ mockedExecFileSync.mockReturnValueOnce('[]');
434
+ // 2. No open children
435
+ mockedExecFileSync.mockReturnValueOnce('[]');
436
+ // 3. Issue info: epic
437
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{ id: 'beth-abc', title: 'Feature', status: 'open', issue_type: 'epic' }]));
438
+ // 4. All children (for test subtask check)
439
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{
440
+ id: 'beth-abc',
441
+ dependents: [
442
+ { id: 'beth-abc.1', title: 'Implement feature', status: 'closed' },
443
+ { id: 'beth-abc.2', title: 'Unit tests for feature', status: 'closed' },
444
+ { id: 'beth-abc.3', title: 'E2E tests for feature', status: 'closed' },
445
+ { id: 'beth-abc.4', title: 'Security tests for feature', status: 'closed' },
446
+ ],
447
+ }]));
448
+ // 5. bd close succeeds
449
+ mockedExecFileSync.mockReturnValueOnce('');
450
+ }
451
+ it('rejects invalid issue IDs', () => {
452
+ const result = closeIssue('INVALID; rm -rf /', {});
453
+ expect(result.success).toBe(false);
454
+ expect(mockedExecFileSync).not.toHaveBeenCalled();
455
+ });
456
+ it('blocks close when open blockers exist', () => {
457
+ const mockDeps = [
458
+ { id: 'beth-xyz', title: 'Open blocker', status: 'open', dependency_type: 'blocks' },
459
+ ];
460
+ // 1. Open blockers returned
461
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify(mockDeps));
462
+ const result = closeIssue('beth-abc.1', {});
463
+ expect(result.success).toBe(false);
464
+ expect(result.blockers).toHaveLength(1);
465
+ expect(result.blockers[0].id).toBe('beth-xyz');
466
+ // Should NOT proceed to children check or close
467
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(1);
468
+ });
469
+ it('blocks close when open children exist', () => {
470
+ const mockChildren = [
471
+ { id: 'beth-abc.1', title: 'Still open', status: 'open' },
472
+ ];
473
+ // 1. No blockers
474
+ mockedExecFileSync.mockReturnValueOnce('[]');
475
+ // 2. Open children found
476
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify(mockChildren));
477
+ const result = closeIssue('beth-abc', {});
478
+ expect(result.success).toBe(false);
479
+ expect(result.blocked).toHaveLength(1);
480
+ expect(result.blocked[0].id).toBe('beth-abc.1');
481
+ // Should NOT proceed to close
482
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(2);
483
+ });
484
+ it('blocks epic close when missing test subtasks', () => {
485
+ // 1. No blockers
486
+ mockedExecFileSync.mockReturnValueOnce('[]');
487
+ // 2. No open children
488
+ mockedExecFileSync.mockReturnValueOnce('[]');
489
+ // 3. Issue is an epic
490
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{ id: 'beth-abc', title: 'Feature', status: 'open', issue_type: 'epic' }]));
491
+ // 4. Children have no test subtasks
492
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{
493
+ id: 'beth-abc',
494
+ dependents: [
495
+ { id: 'beth-abc.1', title: 'Implement feature', status: 'closed' },
496
+ ],
497
+ }]));
498
+ const result = closeIssue('beth-abc', {});
499
+ expect(result.success).toBe(false);
500
+ expect(result.missingTests).toHaveLength(3);
501
+ expect(result.missingTests).toContain('Unit tests');
502
+ expect(result.missingTests).toContain('E2E/Integration tests');
503
+ expect(result.missingTests).toContain('Security tests');
504
+ // Should NOT call bd close
505
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(4);
506
+ });
507
+ it('blocks epic close when partially missing test subtasks', () => {
508
+ // 1. No blockers
509
+ mockedExecFileSync.mockReturnValueOnce('[]');
510
+ // 2. No open children
511
+ mockedExecFileSync.mockReturnValueOnce('[]');
512
+ // 3. Issue is an epic
513
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{ id: 'beth-abc', title: 'Feature', status: 'open', issue_type: 'epic' }]));
514
+ // 4. Has unit tests but missing e2e and security
515
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{
516
+ id: 'beth-abc',
517
+ dependents: [
518
+ { id: 'beth-abc.1', title: 'Implement feature', status: 'closed' },
519
+ { id: 'beth-abc.2', title: 'Unit tests for feature', status: 'closed' },
520
+ ],
521
+ }]));
522
+ const result = closeIssue('beth-abc', {});
523
+ expect(result.success).toBe(false);
524
+ expect(result.missingTests).toHaveLength(2);
525
+ expect(result.missingTests).toContain('E2E/Integration tests');
526
+ expect(result.missingTests).toContain('Security tests');
527
+ });
528
+ it('allows close when no blockers, no children (leaf task)', () => {
529
+ mockCleanLeafTask();
530
+ const result = closeIssue('beth-abc', {});
531
+ expect(result.success).toBe(true);
532
+ // Last call should be bd close
533
+ const lastCall = mockedExecFileSync.mock.calls[mockedExecFileSync.mock.calls.length - 1];
534
+ expect(lastCall[0]).toBe('bd');
535
+ expect(lastCall[1]).toContain('close');
536
+ });
537
+ it('allows epic close when all test subtasks present', () => {
538
+ mockCleanEpicWithTests();
539
+ const result = closeIssue('beth-abc', {});
540
+ expect(result.success).toBe(true);
541
+ // 5 calls: dep list, children, show (info), show (all children), close
542
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(5);
543
+ });
544
+ it('skips test subtask check for non-epic issues', () => {
545
+ mockCleanLeafTask();
546
+ const result = closeIssue('beth-abc', {});
547
+ expect(result.success).toBe(true);
548
+ // 4 calls: dep list, children, show (info), close — no getAllChildren call
549
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(4);
550
+ });
551
+ it('passes --reason to bd close', () => {
552
+ mockCleanLeafTask();
553
+ closeIssue('beth-abc', { reason: 'All done' });
554
+ const lastCall = mockedExecFileSync.mock.calls[mockedExecFileSync.mock.calls.length - 1];
555
+ expect(lastCall[1]).toEqual(['close', 'beth-abc', '--reason', 'All done']);
556
+ });
557
+ it('passes --force to bd close and skips ALL enforcement', () => {
558
+ mockedExecFileSync.mockReturnValueOnce('');
559
+ const result = closeIssue('beth-abc', { force: true });
560
+ expect(result.success).toBe(true);
561
+ // Should have called ONLY bd close (no blocker, children, or epic checks)
562
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(1);
563
+ expect(mockedExecFileSync).toHaveBeenCalledWith('bd', ['close', 'beth-abc', '--force'], expect.objectContaining({ stdio: 'inherit' }));
564
+ });
565
+ it('allows close on leaf issues when bd throws on children', () => {
566
+ // 1. No blockers
567
+ mockedExecFileSync.mockReturnValueOnce('[]');
568
+ // 2. bd children throws (no children exist)
569
+ mockedExecFileSync.mockImplementationOnce(() => {
570
+ throw new Error('no children');
571
+ });
572
+ // 3. Issue info: task
573
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{ id: 'beth-abc.1', title: 'Leaf', status: 'open', issue_type: 'task' }]));
574
+ // 4. bd close succeeds
575
+ mockedExecFileSync.mockReturnValueOnce('');
576
+ const result = closeIssue('beth-abc.1', {});
577
+ expect(result.success).toBe(true);
578
+ });
579
+ it('returns failure when bd close fails', () => {
580
+ // 1. No blockers
581
+ mockedExecFileSync.mockReturnValueOnce('[]');
582
+ // 2. No children
583
+ mockedExecFileSync.mockReturnValueOnce('[]');
584
+ // 3. Issue info: task
585
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify([{ id: 'beth-abc', title: 'Task', status: 'open', issue_type: 'task' }]));
586
+ // 4. bd close fails
587
+ mockedExecFileSync.mockImplementationOnce(() => {
588
+ throw new Error('bd close failed');
589
+ });
590
+ const result = closeIssue('beth-abc', {});
591
+ expect(result.success).toBe(false);
592
+ });
593
+ it('blocks close with multiple open children', () => {
594
+ const mockChildren = [
595
+ { id: 'beth-abc.1', title: 'Task A', status: 'open' },
596
+ { id: 'beth-abc.2', title: 'Task B', status: 'in_progress' },
597
+ { id: 'beth-abc.3', title: 'Task C', status: 'open' },
598
+ ];
599
+ // 1. No blockers
600
+ mockedExecFileSync.mockReturnValueOnce('[]');
601
+ // 2. Multiple open children
602
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify(mockChildren));
603
+ const result = closeIssue('beth-abc', {});
604
+ expect(result.success).toBe(false);
605
+ expect(result.blocked).toHaveLength(3);
606
+ });
607
+ it('gracefully handles bd show failure for issue info', () => {
608
+ // 1. No blockers
609
+ mockedExecFileSync.mockReturnValueOnce('[]');
610
+ // 2. No children
611
+ mockedExecFileSync.mockReturnValueOnce('[]');
612
+ // 3. bd show fails — getIssueInfo returns null → skip epic check
613
+ mockedExecFileSync.mockImplementationOnce(() => {
614
+ throw new Error('bd show failed');
615
+ });
616
+ // 4. bd close succeeds
617
+ mockedExecFileSync.mockReturnValueOnce('');
618
+ const result = closeIssue('beth-abc', {});
619
+ expect(result.success).toBe(true);
620
+ });
621
+ it('prioritizes blocker check over children check', () => {
622
+ const mockDeps = [
623
+ { id: 'beth-xyz', title: 'Blocker', status: 'open', dependency_type: 'blocks' },
624
+ ];
625
+ // 1. Blocker found → stops immediately
626
+ mockedExecFileSync.mockReturnValueOnce(JSON.stringify(mockDeps));
627
+ const result = closeIssue('beth-abc', {});
628
+ expect(result.success).toBe(false);
629
+ expect(result.blockers).toBeDefined();
630
+ // Only 1 call — never checked children
631
+ expect(mockedExecFileSync).toHaveBeenCalledTimes(1);
632
+ });
633
+ });
634
+ //# sourceMappingURL=close.test.js.map