beth-copilot 1.0.17 → 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 (265) hide show
  1. package/CHANGELOG.md +41 -28
  2. package/README.md +87 -247
  3. package/bin/cli.js +115 -7
  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/init-logic.e2e.test.d.ts +37 -0
  39. package/dist/cli/commands/init-logic.e2e.test.d.ts.map +1 -0
  40. package/dist/cli/commands/init-logic.e2e.test.js +305 -0
  41. package/dist/cli/commands/init-logic.e2e.test.js.map +1 -0
  42. package/dist/cli/commands/land.d.ts +142 -0
  43. package/dist/cli/commands/land.d.ts.map +1 -0
  44. package/dist/cli/commands/land.js +647 -0
  45. package/dist/cli/commands/land.js.map +1 -0
  46. package/dist/cli/commands/land.test.d.ts +20 -0
  47. package/dist/cli/commands/land.test.d.ts.map +1 -0
  48. package/dist/cli/commands/land.test.js +622 -0
  49. package/dist/cli/commands/land.test.js.map +1 -0
  50. package/dist/cli/commands/pipeline.e2e.test.js +1 -1
  51. package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
  52. package/dist/cli/commands/pre-push-guard.d.ts +84 -0
  53. package/dist/cli/commands/pre-push-guard.d.ts.map +1 -0
  54. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts +24 -0
  55. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts.map +1 -0
  56. package/dist/cli/commands/pre-push-guard.e2e.test.js +171 -0
  57. package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -0
  58. package/dist/cli/commands/pre-push-guard.js +257 -0
  59. package/dist/cli/commands/pre-push-guard.js.map +1 -0
  60. package/dist/cli/commands/pre-push-guard.test.d.ts +15 -0
  61. package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -0
  62. package/dist/cli/commands/pre-push-guard.test.js +397 -0
  63. package/dist/cli/commands/pre-push-guard.test.js.map +1 -0
  64. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +23 -0
  65. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts.map +1 -0
  66. package/dist/cli/commands/quickstart-expanded.e2e.test.js +179 -0
  67. package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -0
  68. package/dist/cli/commands/quickstart.test.js +40 -2
  69. package/dist/cli/commands/quickstart.test.js.map +1 -1
  70. package/dist/core/agents/suite.test.js +4 -2
  71. package/dist/core/agents/suite.test.js.map +1 -1
  72. package/dist/core/agents/tools.test.js +5 -1
  73. package/dist/core/agents/tools.test.js.map +1 -1
  74. package/dist/index.d.ts +3 -10
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -10
  77. package/dist/index.js.map +1 -1
  78. package/package.json +15 -9
  79. package/sbom.json +2011 -819
  80. package/templates/.github/agents/beth.agent.md +222 -45
  81. package/templates/.github/agents/developer.agent.md +37 -67
  82. package/templates/.github/agents/product-manager.agent.md +15 -57
  83. package/templates/.github/agents/researcher.agent.md +20 -60
  84. package/templates/.github/agents/security-reviewer.agent.md +29 -70
  85. package/templates/.github/agents/tester.agent.md +40 -58
  86. package/templates/.github/agents/ux-designer.agent.md +20 -63
  87. package/templates/.github/copilot-instructions.md +217 -204
  88. package/templates/AGENTS.md +108 -20
  89. package/dist/core/context.d.ts +0 -171
  90. package/dist/core/context.d.ts.map +0 -1
  91. package/dist/core/context.js +0 -353
  92. package/dist/core/context.js.map +0 -1
  93. package/dist/core/context.test.d.ts +0 -8
  94. package/dist/core/context.test.d.ts.map +0 -1
  95. package/dist/core/context.test.js +0 -253
  96. package/dist/core/context.test.js.map +0 -1
  97. package/dist/core/handoffs.d.ts +0 -151
  98. package/dist/core/handoffs.d.ts.map +0 -1
  99. package/dist/core/handoffs.js +0 -220
  100. package/dist/core/handoffs.js.map +0 -1
  101. package/dist/core/handoffs.test.d.ts +0 -8
  102. package/dist/core/handoffs.test.d.ts.map +0 -1
  103. package/dist/core/handoffs.test.js +0 -231
  104. package/dist/core/handoffs.test.js.map +0 -1
  105. package/dist/core/orchestrator.d.ts +0 -246
  106. package/dist/core/orchestrator.d.ts.map +0 -1
  107. package/dist/core/orchestrator.js +0 -514
  108. package/dist/core/orchestrator.js.map +0 -1
  109. package/dist/core/orchestrator.test.d.ts +0 -8
  110. package/dist/core/orchestrator.test.d.ts.map +0 -1
  111. package/dist/core/orchestrator.test.js +0 -517
  112. package/dist/core/orchestrator.test.js.map +0 -1
  113. package/dist/core/router.d.ts +0 -102
  114. package/dist/core/router.d.ts.map +0 -1
  115. package/dist/core/router.js +0 -178
  116. package/dist/core/router.js.map +0 -1
  117. package/dist/core/router.test.d.ts +0 -8
  118. package/dist/core/router.test.d.ts.map +0 -1
  119. package/dist/core/router.test.js +0 -215
  120. package/dist/core/router.test.js.map +0 -1
  121. package/dist/init.test.js +0 -288
  122. package/dist/providers/azure.d.ts +0 -147
  123. package/dist/providers/azure.d.ts.map +0 -1
  124. package/dist/providers/azure.js +0 -491
  125. package/dist/providers/azure.js.map +0 -1
  126. package/dist/providers/azure.test.d.ts +0 -11
  127. package/dist/providers/azure.test.d.ts.map +0 -1
  128. package/dist/providers/azure.test.js +0 -330
  129. package/dist/providers/azure.test.js.map +0 -1
  130. package/dist/providers/config.d.ts +0 -87
  131. package/dist/providers/config.d.ts.map +0 -1
  132. package/dist/providers/config.js +0 -193
  133. package/dist/providers/config.js.map +0 -1
  134. package/dist/providers/config.test.d.ts +0 -7
  135. package/dist/providers/config.test.d.ts.map +0 -1
  136. package/dist/providers/config.test.js +0 -370
  137. package/dist/providers/config.test.js.map +0 -1
  138. package/dist/providers/index.d.ts +0 -18
  139. package/dist/providers/index.d.ts.map +0 -1
  140. package/dist/providers/index.js +0 -14
  141. package/dist/providers/index.js.map +0 -1
  142. package/dist/providers/interface.d.ts +0 -191
  143. package/dist/providers/interface.d.ts.map +0 -1
  144. package/dist/providers/interface.js +0 -94
  145. package/dist/providers/interface.js.map +0 -1
  146. package/dist/providers/retry.d.ts +0 -128
  147. package/dist/providers/retry.d.ts.map +0 -1
  148. package/dist/providers/retry.js +0 -205
  149. package/dist/providers/retry.js.map +0 -1
  150. package/dist/providers/retry.test.d.ts +0 -7
  151. package/dist/providers/retry.test.d.ts.map +0 -1
  152. package/dist/providers/retry.test.js +0 -439
  153. package/dist/providers/retry.test.js.map +0 -1
  154. package/dist/providers/streaming.d.ts +0 -157
  155. package/dist/providers/streaming.d.ts.map +0 -1
  156. package/dist/providers/streaming.js +0 -233
  157. package/dist/providers/streaming.js.map +0 -1
  158. package/dist/providers/streaming.test.d.ts +0 -7
  159. package/dist/providers/streaming.test.d.ts.map +0 -1
  160. package/dist/providers/streaming.test.js +0 -372
  161. package/dist/providers/streaming.test.js.map +0 -1
  162. package/dist/providers/types.d.ts +0 -209
  163. package/dist/providers/types.d.ts.map +0 -1
  164. package/dist/providers/types.js +0 -53
  165. package/dist/providers/types.js.map +0 -1
  166. package/dist/providers/types.test.d.ts +0 -7
  167. package/dist/providers/types.test.d.ts.map +0 -1
  168. package/dist/providers/types.test.js +0 -141
  169. package/dist/providers/types.test.js.map +0 -1
  170. package/dist/tools/cli/beads.d.ts +0 -27
  171. package/dist/tools/cli/beads.d.ts.map +0 -1
  172. package/dist/tools/cli/beads.js +0 -172
  173. package/dist/tools/cli/beads.js.map +0 -1
  174. package/dist/tools/cli/beads.test.d.ts +0 -8
  175. package/dist/tools/cli/beads.test.d.ts.map +0 -1
  176. package/dist/tools/cli/beads.test.js +0 -264
  177. package/dist/tools/cli/beads.test.js.map +0 -1
  178. package/dist/tools/cli/editFile.d.ts +0 -17
  179. package/dist/tools/cli/editFile.d.ts.map +0 -1
  180. package/dist/tools/cli/editFile.js +0 -125
  181. package/dist/tools/cli/editFile.js.map +0 -1
  182. package/dist/tools/cli/editFile.test.d.ts +0 -8
  183. package/dist/tools/cli/editFile.test.d.ts.map +0 -1
  184. package/dist/tools/cli/editFile.test.js +0 -177
  185. package/dist/tools/cli/editFile.test.js.map +0 -1
  186. package/dist/tools/cli/readFile.d.ts +0 -25
  187. package/dist/tools/cli/readFile.d.ts.map +0 -1
  188. package/dist/tools/cli/readFile.js +0 -118
  189. package/dist/tools/cli/readFile.js.map +0 -1
  190. package/dist/tools/cli/readFile.test.d.ts +0 -8
  191. package/dist/tools/cli/readFile.test.d.ts.map +0 -1
  192. package/dist/tools/cli/readFile.test.js +0 -194
  193. package/dist/tools/cli/readFile.test.js.map +0 -1
  194. package/dist/tools/cli/search.d.ts +0 -16
  195. package/dist/tools/cli/search.d.ts.map +0 -1
  196. package/dist/tools/cli/search.js +0 -261
  197. package/dist/tools/cli/search.js.map +0 -1
  198. package/dist/tools/cli/search.test.d.ts +0 -8
  199. package/dist/tools/cli/search.test.d.ts.map +0 -1
  200. package/dist/tools/cli/search.test.js +0 -172
  201. package/dist/tools/cli/search.test.js.map +0 -1
  202. package/dist/tools/cli/subagent.d.ts +0 -43
  203. package/dist/tools/cli/subagent.d.ts.map +0 -1
  204. package/dist/tools/cli/subagent.js +0 -99
  205. package/dist/tools/cli/subagent.js.map +0 -1
  206. package/dist/tools/cli/subagent.test.d.ts +0 -8
  207. package/dist/tools/cli/subagent.test.d.ts.map +0 -1
  208. package/dist/tools/cli/subagent.test.js +0 -190
  209. package/dist/tools/cli/subagent.test.js.map +0 -1
  210. package/dist/tools/cli/terminal.d.ts +0 -19
  211. package/dist/tools/cli/terminal.d.ts.map +0 -1
  212. package/dist/tools/cli/terminal.js +0 -164
  213. package/dist/tools/cli/terminal.js.map +0 -1
  214. package/dist/tools/cli/terminal.test.d.ts +0 -8
  215. package/dist/tools/cli/terminal.test.d.ts.map +0 -1
  216. package/dist/tools/cli/terminal.test.js +0 -161
  217. package/dist/tools/cli/terminal.test.js.map +0 -1
  218. package/dist/tools/index.d.ts +0 -25
  219. package/dist/tools/index.d.ts.map +0 -1
  220. package/dist/tools/index.js +0 -41
  221. package/dist/tools/index.js.map +0 -1
  222. package/dist/tools/interface.d.ts +0 -64
  223. package/dist/tools/interface.d.ts.map +0 -1
  224. package/dist/tools/interface.js +0 -37
  225. package/dist/tools/interface.js.map +0 -1
  226. package/dist/tools/interface.test.d.ts +0 -7
  227. package/dist/tools/interface.test.d.ts.map +0 -1
  228. package/dist/tools/interface.test.js +0 -179
  229. package/dist/tools/interface.test.js.map +0 -1
  230. package/dist/tools/mcp/bridge.d.ts +0 -48
  231. package/dist/tools/mcp/bridge.d.ts.map +0 -1
  232. package/dist/tools/mcp/bridge.js +0 -128
  233. package/dist/tools/mcp/bridge.js.map +0 -1
  234. package/dist/tools/mcp/bridge.test.d.ts +0 -8
  235. package/dist/tools/mcp/bridge.test.d.ts.map +0 -1
  236. package/dist/tools/mcp/bridge.test.js +0 -300
  237. package/dist/tools/mcp/bridge.test.js.map +0 -1
  238. package/dist/tools/mcp/client.d.ts +0 -135
  239. package/dist/tools/mcp/client.d.ts.map +0 -1
  240. package/dist/tools/mcp/client.js +0 -263
  241. package/dist/tools/mcp/client.js.map +0 -1
  242. package/dist/tools/mcp/client.test.d.ts +0 -8
  243. package/dist/tools/mcp/client.test.d.ts.map +0 -1
  244. package/dist/tools/mcp/client.test.js +0 -390
  245. package/dist/tools/mcp/client.test.js.map +0 -1
  246. package/dist/tools/registry.d.ts +0 -82
  247. package/dist/tools/registry.d.ts.map +0 -1
  248. package/dist/tools/registry.js +0 -99
  249. package/dist/tools/registry.js.map +0 -1
  250. package/dist/tools/registry.test.d.ts +0 -7
  251. package/dist/tools/registry.test.d.ts.map +0 -1
  252. package/dist/tools/registry.test.js +0 -199
  253. package/dist/tools/registry.test.js.map +0 -1
  254. package/dist/tools/suite.test.d.ts +0 -11
  255. package/dist/tools/suite.test.d.ts.map +0 -1
  256. package/dist/tools/suite.test.js +0 -119
  257. package/dist/tools/suite.test.js.map +0 -1
  258. package/dist/tools/types.d.ts +0 -75
  259. package/dist/tools/types.d.ts.map +0 -1
  260. package/dist/tools/types.js +0 -30
  261. package/dist/tools/types.js.map +0 -1
  262. package/dist/tools/types.test.d.ts +0 -7
  263. package/dist/tools/types.test.d.ts.map +0 -1
  264. package/dist/tools/types.test.js +0 -178
  265. package/dist/tools/types.test.js.map +0 -1
@@ -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