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.
- package/CHANGELOG.md +41 -28
- package/README.md +87 -247
- package/bin/cli.js +158 -358
- package/dist/__tests__/smoke.test.d.ts +8 -0
- package/dist/__tests__/smoke.test.d.ts.map +1 -0
- package/dist/__tests__/smoke.test.js +49 -0
- package/dist/__tests__/smoke.test.js.map +1 -0
- package/dist/cli/commands/beads.e2e.test.d.ts +13 -0
- package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/beads.e2e.test.js +526 -0
- package/dist/cli/commands/beads.e2e.test.js.map +1 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts +32 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.js +162 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -0
- package/dist/cli/commands/close.d.ts +89 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.e2e.test.d.ts +27 -0
- package/dist/cli/commands/close.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/close.e2e.test.js +252 -0
- package/dist/cli/commands/close.e2e.test.js.map +1 -0
- package/dist/cli/commands/close.js +309 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/close.test.d.ts +15 -0
- package/dist/cli/commands/close.test.d.ts.map +1 -0
- package/dist/cli/commands/close.test.js +634 -0
- package/dist/cli/commands/close.test.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +23 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +93 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/doctor.test.js +209 -0
- package/dist/cli/commands/doctor.test.js.map +1 -1
- package/dist/cli/commands/framework-isolation.test.d.ts +30 -0
- package/dist/cli/commands/framework-isolation.test.d.ts.map +1 -0
- package/dist/cli/commands/framework-isolation.test.js +119 -0
- package/dist/cli/commands/framework-isolation.test.js.map +1 -0
- package/dist/cli/commands/help.e2e.test.js +4 -4
- package/dist/cli/commands/help.e2e.test.js.map +1 -1
- package/dist/cli/commands/init-logic.e2e.test.d.ts +37 -0
- package/dist/cli/commands/init-logic.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/init-logic.e2e.test.js +305 -0
- package/dist/cli/commands/init-logic.e2e.test.js.map +1 -0
- package/dist/cli/commands/land.d.ts +142 -0
- package/dist/cli/commands/land.d.ts.map +1 -0
- package/dist/cli/commands/land.js +647 -0
- package/dist/cli/commands/land.js.map +1 -0
- package/dist/cli/commands/land.test.d.ts +20 -0
- package/dist/cli/commands/land.test.d.ts.map +1 -0
- package/dist/cli/commands/land.test.js +622 -0
- package/dist/cli/commands/land.test.js.map +1 -0
- package/dist/cli/commands/mcp.e2e.test.js +22 -29
- package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
- package/dist/cli/commands/pipeline.e2e.test.js +20 -20
- package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
- package/dist/cli/commands/pre-push-guard.d.ts +84 -0
- package/dist/cli/commands/pre-push-guard.d.ts.map +1 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.d.ts +24 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.js +171 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -0
- package/dist/cli/commands/pre-push-guard.js +257 -0
- package/dist/cli/commands/pre-push-guard.js.map +1 -0
- package/dist/cli/commands/pre-push-guard.test.d.ts +15 -0
- package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -0
- package/dist/cli/commands/pre-push-guard.test.js +397 -0
- package/dist/cli/commands/pre-push-guard.test.js.map +1 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +23 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.js +179 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -0
- package/dist/cli/commands/quickstart.d.ts.map +1 -1
- package/dist/cli/commands/quickstart.js +7 -23
- package/dist/cli/commands/quickstart.js.map +1 -1
- package/dist/cli/commands/quickstart.test.js +40 -67
- package/dist/cli/commands/quickstart.test.js.map +1 -1
- package/dist/core/agents/suite.test.js +4 -2
- package/dist/core/agents/suite.test.js.map +1 -1
- package/dist/core/agents/tools.test.js +5 -1
- package/dist/core/agents/tools.test.js.map +1 -1
- package/dist/index.d.ts +3 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -10
- package/dist/index.js.map +1 -1
- package/package.json +15 -9
- package/sbom.json +2011 -819
- package/templates/.github/agents/beth.agent.md +220 -66
- package/templates/.github/agents/developer.agent.md +53 -90
- package/templates/.github/agents/product-manager.agent.md +15 -68
- package/templates/.github/agents/researcher.agent.md +20 -71
- package/templates/.github/agents/security-reviewer.agent.md +29 -81
- package/templates/.github/agents/tester.agent.md +40 -69
- package/templates/.github/agents/ux-designer.agent.md +20 -74
- package/templates/.github/copilot-instructions.md +217 -225
- package/templates/AGENTS.md +108 -20
- package/templates/mcp.json.example +0 -3
- package/dist/cli/commands/client-config.d.ts +0 -31
- package/dist/cli/commands/client-config.d.ts.map +0 -1
- package/dist/cli/commands/client-config.e2e.test.d.ts +0 -15
- package/dist/cli/commands/client-config.e2e.test.d.ts.map +0 -1
- package/dist/cli/commands/client-config.e2e.test.js +0 -556
- package/dist/cli/commands/client-config.e2e.test.js.map +0 -1
- package/dist/cli/commands/client-config.js +0 -73
- package/dist/cli/commands/client-config.js.map +0 -1
- package/dist/cli/commands/client-config.test.d.ts +0 -6
- package/dist/cli/commands/client-config.test.d.ts.map +0 -1
- package/dist/cli/commands/client-config.test.js +0 -133
- package/dist/cli/commands/client-config.test.js.map +0 -1
- package/dist/cli/commands/init-quickstart.e2e.test.d.ts +0 -11
- package/dist/cli/commands/init-quickstart.e2e.test.d.ts.map +0 -1
- package/dist/cli/commands/init-quickstart.e2e.test.js +0 -221
- package/dist/cli/commands/init-quickstart.e2e.test.js.map +0 -1
- package/dist/core/context.d.ts +0 -171
- package/dist/core/context.d.ts.map +0 -1
- package/dist/core/context.js +0 -353
- package/dist/core/context.js.map +0 -1
- package/dist/core/context.test.d.ts +0 -8
- package/dist/core/context.test.d.ts.map +0 -1
- package/dist/core/context.test.js +0 -253
- package/dist/core/context.test.js.map +0 -1
- package/dist/core/handoffs.d.ts +0 -151
- package/dist/core/handoffs.d.ts.map +0 -1
- package/dist/core/handoffs.js +0 -220
- package/dist/core/handoffs.js.map +0 -1
- package/dist/core/handoffs.test.d.ts +0 -8
- package/dist/core/handoffs.test.d.ts.map +0 -1
- package/dist/core/handoffs.test.js +0 -231
- package/dist/core/handoffs.test.js.map +0 -1
- package/dist/core/orchestrator.d.ts +0 -246
- package/dist/core/orchestrator.d.ts.map +0 -1
- package/dist/core/orchestrator.js +0 -514
- package/dist/core/orchestrator.js.map +0 -1
- package/dist/core/orchestrator.test.d.ts +0 -8
- package/dist/core/orchestrator.test.d.ts.map +0 -1
- package/dist/core/orchestrator.test.js +0 -517
- package/dist/core/orchestrator.test.js.map +0 -1
- package/dist/core/router.d.ts +0 -102
- package/dist/core/router.d.ts.map +0 -1
- package/dist/core/router.js +0 -178
- package/dist/core/router.js.map +0 -1
- package/dist/core/router.test.d.ts +0 -8
- package/dist/core/router.test.d.ts.map +0 -1
- package/dist/core/router.test.js +0 -215
- package/dist/core/router.test.js.map +0 -1
- package/dist/init.test.js +0 -288
- package/dist/providers/azure.d.ts +0 -147
- package/dist/providers/azure.d.ts.map +0 -1
- package/dist/providers/azure.js +0 -491
- package/dist/providers/azure.js.map +0 -1
- package/dist/providers/azure.test.d.ts +0 -11
- package/dist/providers/azure.test.d.ts.map +0 -1
- package/dist/providers/azure.test.js +0 -330
- package/dist/providers/azure.test.js.map +0 -1
- package/dist/providers/config.d.ts +0 -87
- package/dist/providers/config.d.ts.map +0 -1
- package/dist/providers/config.js +0 -193
- package/dist/providers/config.js.map +0 -1
- package/dist/providers/config.test.d.ts +0 -7
- package/dist/providers/config.test.d.ts.map +0 -1
- package/dist/providers/config.test.js +0 -370
- package/dist/providers/config.test.js.map +0 -1
- package/dist/providers/index.d.ts +0 -18
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -14
- package/dist/providers/index.js.map +0 -1
- package/dist/providers/interface.d.ts +0 -191
- package/dist/providers/interface.d.ts.map +0 -1
- package/dist/providers/interface.js +0 -94
- package/dist/providers/interface.js.map +0 -1
- package/dist/providers/retry.d.ts +0 -128
- package/dist/providers/retry.d.ts.map +0 -1
- package/dist/providers/retry.js +0 -205
- package/dist/providers/retry.js.map +0 -1
- package/dist/providers/retry.test.d.ts +0 -7
- package/dist/providers/retry.test.d.ts.map +0 -1
- package/dist/providers/retry.test.js +0 -439
- package/dist/providers/retry.test.js.map +0 -1
- package/dist/providers/streaming.d.ts +0 -157
- package/dist/providers/streaming.d.ts.map +0 -1
- package/dist/providers/streaming.js +0 -233
- package/dist/providers/streaming.js.map +0 -1
- package/dist/providers/streaming.test.d.ts +0 -7
- package/dist/providers/streaming.test.d.ts.map +0 -1
- package/dist/providers/streaming.test.js +0 -372
- package/dist/providers/streaming.test.js.map +0 -1
- package/dist/providers/types.d.ts +0 -209
- package/dist/providers/types.d.ts.map +0 -1
- package/dist/providers/types.js +0 -53
- package/dist/providers/types.js.map +0 -1
- package/dist/providers/types.test.d.ts +0 -7
- package/dist/providers/types.test.d.ts.map +0 -1
- package/dist/providers/types.test.js +0 -141
- package/dist/providers/types.test.js.map +0 -1
- package/dist/tools/cli/beads.d.ts +0 -27
- package/dist/tools/cli/beads.d.ts.map +0 -1
- package/dist/tools/cli/beads.js +0 -172
- package/dist/tools/cli/beads.js.map +0 -1
- package/dist/tools/cli/beads.test.d.ts +0 -8
- package/dist/tools/cli/beads.test.d.ts.map +0 -1
- package/dist/tools/cli/beads.test.js +0 -264
- package/dist/tools/cli/beads.test.js.map +0 -1
- package/dist/tools/cli/editFile.d.ts +0 -17
- package/dist/tools/cli/editFile.d.ts.map +0 -1
- package/dist/tools/cli/editFile.js +0 -125
- package/dist/tools/cli/editFile.js.map +0 -1
- package/dist/tools/cli/editFile.test.d.ts +0 -8
- package/dist/tools/cli/editFile.test.d.ts.map +0 -1
- package/dist/tools/cli/editFile.test.js +0 -177
- package/dist/tools/cli/editFile.test.js.map +0 -1
- package/dist/tools/cli/readFile.d.ts +0 -25
- package/dist/tools/cli/readFile.d.ts.map +0 -1
- package/dist/tools/cli/readFile.js +0 -118
- package/dist/tools/cli/readFile.js.map +0 -1
- package/dist/tools/cli/readFile.test.d.ts +0 -8
- package/dist/tools/cli/readFile.test.d.ts.map +0 -1
- package/dist/tools/cli/readFile.test.js +0 -194
- package/dist/tools/cli/readFile.test.js.map +0 -1
- package/dist/tools/cli/search.d.ts +0 -16
- package/dist/tools/cli/search.d.ts.map +0 -1
- package/dist/tools/cli/search.js +0 -261
- package/dist/tools/cli/search.js.map +0 -1
- package/dist/tools/cli/search.test.d.ts +0 -8
- package/dist/tools/cli/search.test.d.ts.map +0 -1
- package/dist/tools/cli/search.test.js +0 -172
- package/dist/tools/cli/search.test.js.map +0 -1
- package/dist/tools/cli/subagent.d.ts +0 -43
- package/dist/tools/cli/subagent.d.ts.map +0 -1
- package/dist/tools/cli/subagent.js +0 -99
- package/dist/tools/cli/subagent.js.map +0 -1
- package/dist/tools/cli/subagent.test.d.ts +0 -8
- package/dist/tools/cli/subagent.test.d.ts.map +0 -1
- package/dist/tools/cli/subagent.test.js +0 -190
- package/dist/tools/cli/subagent.test.js.map +0 -1
- package/dist/tools/cli/terminal.d.ts +0 -19
- package/dist/tools/cli/terminal.d.ts.map +0 -1
- package/dist/tools/cli/terminal.js +0 -164
- package/dist/tools/cli/terminal.js.map +0 -1
- package/dist/tools/cli/terminal.test.d.ts +0 -8
- package/dist/tools/cli/terminal.test.d.ts.map +0 -1
- package/dist/tools/cli/terminal.test.js +0 -161
- package/dist/tools/cli/terminal.test.js.map +0 -1
- package/dist/tools/index.d.ts +0 -25
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -41
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/interface.d.ts +0 -64
- package/dist/tools/interface.d.ts.map +0 -1
- package/dist/tools/interface.js +0 -37
- package/dist/tools/interface.js.map +0 -1
- package/dist/tools/interface.test.d.ts +0 -7
- package/dist/tools/interface.test.d.ts.map +0 -1
- package/dist/tools/interface.test.js +0 -179
- package/dist/tools/interface.test.js.map +0 -1
- package/dist/tools/mcp/bridge.d.ts +0 -48
- package/dist/tools/mcp/bridge.d.ts.map +0 -1
- package/dist/tools/mcp/bridge.js +0 -128
- package/dist/tools/mcp/bridge.js.map +0 -1
- package/dist/tools/mcp/bridge.test.d.ts +0 -8
- package/dist/tools/mcp/bridge.test.d.ts.map +0 -1
- package/dist/tools/mcp/bridge.test.js +0 -300
- package/dist/tools/mcp/bridge.test.js.map +0 -1
- package/dist/tools/mcp/client.d.ts +0 -135
- package/dist/tools/mcp/client.d.ts.map +0 -1
- package/dist/tools/mcp/client.js +0 -263
- package/dist/tools/mcp/client.js.map +0 -1
- package/dist/tools/mcp/client.test.d.ts +0 -8
- package/dist/tools/mcp/client.test.d.ts.map +0 -1
- package/dist/tools/mcp/client.test.js +0 -390
- package/dist/tools/mcp/client.test.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -82
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -99
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/registry.test.d.ts +0 -7
- package/dist/tools/registry.test.d.ts.map +0 -1
- package/dist/tools/registry.test.js +0 -199
- package/dist/tools/registry.test.js.map +0 -1
- package/dist/tools/suite.test.d.ts +0 -11
- package/dist/tools/suite.test.d.ts.map +0 -1
- package/dist/tools/suite.test.js +0 -119
- package/dist/tools/suite.test.js.map +0 -1
- package/dist/tools/types.d.ts +0 -75
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -30
- package/dist/tools/types.js.map +0 -1
- package/dist/tools/types.test.d.ts +0 -7
- package/dist/tools/types.test.d.ts.map +0 -1
- package/dist/tools/types.test.js +0 -178
- package/dist/tools/types.test.js.map +0 -1
- package/templates/.vscode/mcp.json +0 -20
- 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
|