@codemcp/workflows 4.10.12 → 4.12.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 (33) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/plugin-system/commit-plugin.d.ts +40 -0
  3. package/dist/plugin-system/commit-plugin.d.ts.map +1 -0
  4. package/dist/plugin-system/commit-plugin.js +204 -0
  5. package/dist/plugin-system/commit-plugin.js.map +1 -0
  6. package/dist/plugin-system/plugin-interfaces.d.ts +0 -1
  7. package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -1
  8. package/dist/server-config.d.ts.map +1 -1
  9. package/dist/server-config.js +28 -15
  10. package/dist/server-config.js.map +1 -1
  11. package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
  12. package/dist/tool-handlers/proceed-to-phase.js +2 -9
  13. package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
  14. package/dist/tool-handlers/start-development.d.ts +0 -1
  15. package/dist/tool-handlers/start-development.d.ts.map +1 -1
  16. package/dist/tool-handlers/start-development.js +25 -22
  17. package/dist/tool-handlers/start-development.js.map +1 -1
  18. package/dist/tool-handlers/whats-next.d.ts.map +1 -1
  19. package/dist/tool-handlers/whats-next.js +2 -8
  20. package/dist/tool-handlers/whats-next.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/plugin-system/commit-plugin.ts +252 -0
  23. package/src/plugin-system/plugin-interfaces.ts +0 -1
  24. package/src/server-config.ts +36 -16
  25. package/src/tool-handlers/proceed-to-phase.ts +2 -12
  26. package/src/tool-handlers/start-development.ts +39 -29
  27. package/src/tool-handlers/whats-next.ts +2 -11
  28. package/test/e2e/beads-plugin-integration.test.ts +41 -12
  29. package/test/e2e/commit-plugin-integration.test.ts +222 -0
  30. package/test/unit/commit-plugin.test.ts +196 -0
  31. package/test/unit/server-config-plugin-registry.test.ts +26 -8
  32. package/tsconfig.build.tsbuildinfo +1 -1
  33. package/test/unit/commit-behaviour-interface.test.ts +0 -244
@@ -170,22 +170,13 @@ export class WhatsNextHandler extends ConversationRequiredToolHandler<
170
170
  }
171
171
  );
172
172
 
173
- // Add commit instructions if configured
174
- let finalInstructions = instructions.instructions;
175
- if (
176
- conversationContext.gitCommitConfig?.enabled &&
177
- conversationContext.gitCommitConfig.commitOnStep
178
- ) {
179
- const commitMessage = requestContext || 'Step completion';
180
- finalInstructions += `\n\n**Git Commit Required**: Create a commit for this step using:\n\`\`\`bash\ngit add . && git commit -m "${commitMessage}"\n\`\`\``;
181
- }
182
-
173
+ // Note: Commit behavior now handled by CommitPlugin
183
174
  // Note: Beads-specific instructions are now handled by BeadsInstructionGenerator via strategy pattern
184
175
 
185
176
  // Prepare response
186
177
  const response: WhatsNextResult = {
187
178
  phase: transitionResult.newPhase,
188
- instructions: finalInstructions,
179
+ instructions: instructions.instructions,
189
180
  plan_file_path: conversationContext.planFilePath,
190
181
  };
191
182
 
@@ -418,9 +418,9 @@ describe('Beads Plugin Comprehensive Integration', () => {
418
418
  );
419
419
 
420
420
  await cleanupWith();
421
- delete process.env.TASK_BACKEND;
422
421
 
423
- // WITHOUT BEADS
422
+ // WITHOUT BEADS - explicitly set markdown to disable auto-detection
423
+ process.env.TASK_BACKEND = 'markdown';
424
424
  const scenarioWithout = await createSuiteIsolatedE2EScenario({
425
425
  suiteName: 'beads-comparison-without',
426
426
  tempProjectFactory: createTempProjectWithDefaultStateMachine,
@@ -441,6 +441,7 @@ describe('Beads Plugin Comprehensive Integration', () => {
441
441
  );
442
442
 
443
443
  await cleanupWithout();
444
+ delete process.env.TASK_BACKEND;
444
445
 
445
446
  // VALIDATE: WITH beads has beads markers
446
447
  expect(planContentWith).toContain('<!-- beads-phase-id:');
@@ -477,9 +478,9 @@ describe('Beads Plugin Comprehensive Integration', () => {
477
478
  const instructionsWithBeads = responseWith.instructions;
478
479
 
479
480
  await scenarioWith.cleanup();
480
- delete process.env.TASK_BACKEND;
481
481
 
482
- // WITHOUT BEADS
482
+ // WITHOUT BEADS - explicitly set markdown to disable auto-detection
483
+ process.env.TASK_BACKEND = 'markdown';
483
484
  const scenarioWithout = await createSuiteIsolatedE2EScenario({
484
485
  suiteName: 'beads-instructions-comparison-without',
485
486
  tempProjectFactory: createTempProjectWithDefaultStateMachine,
@@ -503,6 +504,7 @@ describe('Beads Plugin Comprehensive Integration', () => {
503
504
  const instructionsWithout = responseWithout.instructions;
504
505
 
505
506
  await scenarioWithout.cleanup();
507
+ delete process.env.TASK_BACKEND;
506
508
 
507
509
  // VALIDATE: WITH beads mentions bd CLI
508
510
  expect(instructionsWithBeads.toLowerCase()).toContain('bd');
@@ -863,12 +865,13 @@ describe('Beads Plugin Comprehensive Integration', () => {
863
865
  expect(planContent).toContain('<!-- beads-phase-id:');
864
866
  });
865
867
 
866
- it('should NOT apply beads when TASK_BACKEND is not set', async () => {
867
- // Ensure env var is NOT set
868
+ it('should auto-detect and apply beads when TASK_BACKEND is not set and bd is available', async () => {
869
+ // Ensure env var is NOT set - auto-detection will check for bd command
868
870
  delete process.env.TASK_BACKEND;
871
+ setupBeadsCliMock();
869
872
 
870
873
  const scenario = await createSuiteIsolatedE2EScenario({
871
- suiteName: 'beads-activation-without',
874
+ suiteName: 'beads-activation-auto',
872
875
  tempProjectFactory: createTempProjectWithDefaultStateMachine,
873
876
  });
874
877
 
@@ -882,15 +885,41 @@ describe('Beads Plugin Comprehensive Integration', () => {
882
885
 
883
886
  await scenario.cleanup();
884
887
 
885
- // VALIDATE: Beads features NOT enabled
886
- expect(planContent).not.toContain('<!-- beads-phase-id:');
888
+ // VALIDATE: Beads features auto-detected and enabled
889
+ expect(planContent).toContain('<!-- beads-phase-id:');
887
890
  });
888
891
 
889
- it('should NOT apply beads when TASK_BACKEND has different value', async () => {
892
+ it('should auto-detect beads when TASK_BACKEND has invalid value and bd is available', async () => {
893
+ // Invalid values are treated as "not set" - triggers auto-detection
890
894
  process.env.TASK_BACKEND = 'other-backend';
895
+ setupBeadsCliMock();
896
+
897
+ const scenario = await createSuiteIsolatedE2EScenario({
898
+ suiteName: 'beads-activation-invalid',
899
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
900
+ });
901
+
902
+ const result = await scenario.client.callTool('start_development', {
903
+ workflow: 'epcc',
904
+ commit_behaviour: 'none',
905
+ });
906
+
907
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
908
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
909
+
910
+ await scenario.cleanup();
911
+ delete process.env.TASK_BACKEND;
912
+
913
+ // VALIDATE: Beads features auto-detected and enabled
914
+ expect(planContent).toContain('<!-- beads-phase-id:');
915
+ });
916
+
917
+ it('should NOT apply beads when TASK_BACKEND=markdown explicitly', async () => {
918
+ // Explicitly setting markdown should disable beads even if bd is available
919
+ process.env.TASK_BACKEND = 'markdown';
891
920
 
892
921
  const scenario = await createSuiteIsolatedE2EScenario({
893
- suiteName: 'beads-activation-other',
922
+ suiteName: 'beads-activation-disabled',
894
923
  tempProjectFactory: createTempProjectWithDefaultStateMachine,
895
924
  });
896
925
 
@@ -905,7 +934,7 @@ describe('Beads Plugin Comprehensive Integration', () => {
905
934
  await scenario.cleanup();
906
935
  delete process.env.TASK_BACKEND;
907
936
 
908
- // VALIDATE: Beads features NOT enabled
937
+ // VALIDATE: Beads features NOT enabled when markdown explicitly set
909
938
  expect(planContent).not.toContain('<!-- beads-phase-id:');
910
939
  });
911
940
  });
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Integration test for CommitPlugin end-to-end behavior
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { execSync } from 'node:child_process';
7
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+ import { initializeServerComponents } from '../../src/server-config.js';
10
+
11
+ describe('CommitPlugin Integration', () => {
12
+ const testDir = resolve(__dirname, 'test-commit-plugin');
13
+ let originalEnv: Record<string, string | undefined>;
14
+
15
+ beforeEach(() => {
16
+ // Save original environment
17
+ originalEnv = {
18
+ COMMIT_BEHAVIOR: process.env.COMMIT_BEHAVIOR,
19
+ COMMIT_MESSAGE_TEMPLATE: process.env.COMMIT_MESSAGE_TEMPLATE,
20
+ };
21
+
22
+ // Clean up any existing test directory
23
+ try {
24
+ rmSync(testDir, { recursive: true, force: true });
25
+ } catch {
26
+ // Ignore if directory doesn't exist
27
+ }
28
+
29
+ // Create test git repository
30
+ mkdirSync(testDir, { recursive: true });
31
+ execSync('git init', { cwd: testDir });
32
+ execSync('git config user.name "Test User"', { cwd: testDir });
33
+ execSync('git config user.email "test@example.com"', { cwd: testDir });
34
+
35
+ // Create initial commit
36
+ writeFileSync(resolve(testDir, 'README.md'), '# Test Project\n');
37
+ execSync('git add .', { cwd: testDir });
38
+ execSync('git commit -m "Initial commit"', { cwd: testDir });
39
+ });
40
+
41
+ afterEach(() => {
42
+ // Restore original environment
43
+ for (const [key, value] of Object.entries(originalEnv)) {
44
+ if (value === undefined) {
45
+ delete process.env[key];
46
+ } else {
47
+ process.env[key] = value;
48
+ }
49
+ }
50
+
51
+ // Clean up test directory
52
+ try {
53
+ rmSync(testDir, { recursive: true, force: true });
54
+ } catch {
55
+ // Ignore cleanup errors
56
+ }
57
+ });
58
+
59
+ it('should register CommitPlugin when COMMIT_BEHAVIOR is set', async () => {
60
+ // Arrange
61
+ process.env.COMMIT_BEHAVIOR = 'step';
62
+
63
+ // Act
64
+ const components = await initializeServerComponents({
65
+ projectPath: testDir,
66
+ });
67
+
68
+ // Assert
69
+ expect(components.context.pluginRegistry).toBeDefined();
70
+ const plugins = components.context.pluginRegistry.getEnabledPlugins();
71
+ const commitPlugin = plugins.find(p => p.getName() === 'CommitPlugin');
72
+ expect(commitPlugin).toBeDefined();
73
+ expect(commitPlugin?.getSequence()).toBe(50);
74
+ });
75
+
76
+ it('should not register CommitPlugin when COMMIT_BEHAVIOR is not set', async () => {
77
+ // Arrange - no COMMIT_BEHAVIOR set
78
+
79
+ // Act
80
+ const components = await initializeServerComponents({
81
+ projectPath: testDir,
82
+ });
83
+
84
+ // Assert
85
+ const plugins = components.context.pluginRegistry.getEnabledPlugins();
86
+ const commitPlugin = plugins.find(p => p.getName() === 'CommitPlugin');
87
+ expect(commitPlugin).toBeUndefined();
88
+ });
89
+
90
+ it('should add final commit task to plan file when COMMIT_BEHAVIOR=end', async () => {
91
+ // Arrange
92
+ process.env.COMMIT_BEHAVIOR = 'end';
93
+ // Don't set COMMIT_MESSAGE_TEMPLATE to use default message
94
+
95
+ // Act
96
+ const components = await initializeServerComponents({
97
+ projectPath: testDir,
98
+ });
99
+
100
+ // Create a mock plan file content
101
+ const mockPlanContent = `# Test Plan
102
+
103
+ ## Explore
104
+ ### Tasks
105
+ - [ ] Research the problem
106
+
107
+ ## Code
108
+ ### Tasks
109
+ - [ ] Implement solution
110
+
111
+ ## Commit
112
+ ### Tasks
113
+ - [ ] Review implementation
114
+ `;
115
+
116
+ // Execute the afterPlanFileCreated hook
117
+ const plugins = components.context.pluginRegistry.getEnabledPlugins();
118
+ const commitPlugin = plugins.find(p => p.getName() === 'CommitPlugin');
119
+ const hooks = commitPlugin?.getHooks();
120
+
121
+ if (hooks?.afterPlanFileCreated) {
122
+ const mockContext = {
123
+ conversationId: 'test',
124
+ planFilePath: resolve(testDir, 'plan.md'),
125
+ currentPhase: 'explore',
126
+ workflow: 'epcc',
127
+ projectPath: testDir,
128
+ gitBranch: 'main',
129
+ };
130
+
131
+ const updatedContent = await hooks.afterPlanFileCreated(
132
+ mockContext,
133
+ resolve(testDir, 'plan.md'),
134
+ mockPlanContent
135
+ );
136
+
137
+ // Assert
138
+ expect(updatedContent).toContain('Create a conventional commit');
139
+ expect(updatedContent).toContain(
140
+ 'summarize the intentions and key decisions'
141
+ );
142
+ }
143
+ });
144
+
145
+ it('should add squash commit task for step/phase modes', async () => {
146
+ // Arrange
147
+ process.env.COMMIT_BEHAVIOR = 'step';
148
+
149
+ // Act
150
+ const components = await initializeServerComponents({
151
+ projectPath: testDir,
152
+ });
153
+
154
+ const mockPlanContent = `## Commit
155
+ ### Tasks
156
+ - [ ] Review implementation
157
+ `;
158
+
159
+ const plugins = components.context.pluginRegistry.getEnabledPlugins();
160
+ const commitPlugin = plugins.find(p => p.getName() === 'CommitPlugin');
161
+ const hooks = commitPlugin?.getHooks();
162
+
163
+ if (hooks?.afterPlanFileCreated) {
164
+ const mockContext = {
165
+ conversationId: 'test',
166
+ planFilePath: resolve(testDir, 'plan.md'),
167
+ currentPhase: 'explore',
168
+ workflow: 'epcc',
169
+ projectPath: testDir,
170
+ gitBranch: 'main',
171
+ };
172
+
173
+ const updatedContent = await hooks.afterPlanFileCreated(
174
+ mockContext,
175
+ resolve(testDir, 'plan.md'),
176
+ mockPlanContent
177
+ );
178
+
179
+ // Assert
180
+ expect(updatedContent).toContain('Squash WIP commits:');
181
+ expect(updatedContent).toContain('git reset --soft');
182
+ }
183
+ });
184
+
185
+ it('should create WIP commit on phase transition', async () => {
186
+ // Arrange
187
+ process.env.COMMIT_BEHAVIOR = 'phase';
188
+
189
+ // Create some changes
190
+ writeFileSync(resolve(testDir, 'test.txt'), 'test content');
191
+
192
+ // Act
193
+ const components = await initializeServerComponents({
194
+ projectPath: testDir,
195
+ });
196
+
197
+ const plugins = components.context.pluginRegistry.getEnabledPlugins();
198
+ const commitPlugin = plugins.find(p => p.getName() === 'CommitPlugin');
199
+ const hooks = commitPlugin?.getHooks();
200
+
201
+ if (hooks?.beforePhaseTransition) {
202
+ const mockContext = {
203
+ conversationId: 'test',
204
+ planFilePath: resolve(testDir, 'plan.md'),
205
+ currentPhase: 'explore',
206
+ workflow: 'epcc',
207
+ projectPath: testDir,
208
+ gitBranch: 'main',
209
+ targetPhase: 'plan',
210
+ };
211
+
212
+ await hooks.beforePhaseTransition(mockContext, 'explore', 'plan');
213
+
214
+ // Assert - check git log for WIP commit
215
+ const gitLog = execSync('git log --oneline', {
216
+ cwd: testDir,
217
+ encoding: 'utf-8',
218
+ });
219
+ expect(gitLog).toContain('WIP: transition to plan');
220
+ }
221
+ });
222
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Test CommitPlugin activation and lifecycle hooks
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+ import { CommitPlugin } from '../../src/plugin-system/commit-plugin.js';
7
+ import type { PluginHookContext } from '../../src/plugin-system/plugin-interfaces.js';
8
+
9
+ // Mock GitManager
10
+ vi.mock('@codemcp/workflows-core', async () => {
11
+ const actual = await vi.importActual('@codemcp/workflows-core');
12
+ return {
13
+ ...actual,
14
+ GitManager: {
15
+ isGitRepository: vi.fn(),
16
+ hasUncommittedChanges: vi.fn(),
17
+ createCommit: vi.fn(),
18
+ },
19
+ };
20
+ });
21
+
22
+ describe('CommitPlugin', () => {
23
+ let plugin: CommitPlugin;
24
+ const projectPath = '/test/project';
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ // Clear environment variables
29
+ delete process.env.COMMIT_BEHAVIOR;
30
+ delete process.env.COMMIT_MESSAGE_TEMPLATE;
31
+ });
32
+
33
+ describe('Plugin Interface', () => {
34
+ it('should have correct name and sequence', () => {
35
+ plugin = new CommitPlugin({ projectPath });
36
+
37
+ expect(plugin.getName()).toBe('CommitPlugin');
38
+ expect(plugin.getSequence()).toBe(50); // Before BeadsPlugin (100)
39
+ });
40
+
41
+ it('should be enabled when COMMIT_BEHAVIOR is set', () => {
42
+ process.env.COMMIT_BEHAVIOR = 'step';
43
+ plugin = new CommitPlugin({ projectPath });
44
+
45
+ expect(plugin.isEnabled()).toBe(true);
46
+ });
47
+
48
+ it('should be disabled when COMMIT_BEHAVIOR is not set', () => {
49
+ plugin = new CommitPlugin({ projectPath });
50
+
51
+ expect(plugin.isEnabled()).toBe(false);
52
+ });
53
+
54
+ it('should be disabled when COMMIT_BEHAVIOR is invalid', () => {
55
+ process.env.COMMIT_BEHAVIOR = 'invalid';
56
+ plugin = new CommitPlugin({ projectPath });
57
+
58
+ expect(plugin.isEnabled()).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe('Lifecycle Hooks', () => {
63
+ beforeEach(() => {
64
+ process.env.COMMIT_BEHAVIOR = 'step';
65
+ plugin = new CommitPlugin({ projectPath });
66
+ });
67
+
68
+ it('should provide afterStartDevelopment hook', () => {
69
+ const hooks = plugin.getHooks();
70
+
71
+ expect(hooks.afterStartDevelopment).toBeDefined();
72
+ expect(typeof hooks.afterStartDevelopment).toBe('function');
73
+ });
74
+
75
+ it('should provide beforePhaseTransition hook', () => {
76
+ const hooks = plugin.getHooks();
77
+
78
+ expect(hooks.beforePhaseTransition).toBeDefined();
79
+ expect(typeof hooks.beforePhaseTransition).toBe('function');
80
+ });
81
+
82
+ it('should provide afterPlanFileCreated hook', () => {
83
+ const hooks = plugin.getHooks();
84
+
85
+ expect(hooks.afterPlanFileCreated).toBeDefined();
86
+ expect(typeof hooks.afterPlanFileCreated).toBe('function');
87
+ });
88
+ });
89
+
90
+ describe('Step Commit Behavior', () => {
91
+ beforeEach(() => {
92
+ process.env.COMMIT_BEHAVIOR = 'step';
93
+ plugin = new CommitPlugin({ projectPath });
94
+ });
95
+
96
+ it('should create WIP commit on whats_next calls', async () => {
97
+ const { GitManager } = await import('@codemcp/workflows-core');
98
+ vi.mocked(GitManager.isGitRepository).mockReturnValue(true);
99
+ vi.mocked(GitManager.hasUncommittedChanges).mockReturnValue(true);
100
+ vi.mocked(GitManager.createCommit).mockReturnValue(true);
101
+
102
+ const context: PluginHookContext = {
103
+ conversationId: 'test-conv',
104
+ planFilePath: '/test/plan.md',
105
+ currentPhase: 'explore',
106
+ workflow: 'epcc',
107
+ projectPath,
108
+ gitBranch: 'feature/test',
109
+ };
110
+
111
+ const hooks = plugin.getHooks();
112
+ await hooks.afterStartDevelopment?.(
113
+ context,
114
+ { workflow: 'epcc' },
115
+ {
116
+ conversationId: 'test-conv',
117
+ planFilePath: '/test/plan.md',
118
+ phase: 'explore',
119
+ workflow: 'epcc',
120
+ }
121
+ );
122
+
123
+ // Should store initial commit hash for later squashing
124
+ expect(GitManager.isGitRepository).toHaveBeenCalledWith(projectPath);
125
+ });
126
+ });
127
+
128
+ describe('Phase Commit Behavior', () => {
129
+ beforeEach(() => {
130
+ process.env.COMMIT_BEHAVIOR = 'phase';
131
+ plugin = new CommitPlugin({ projectPath });
132
+ });
133
+
134
+ it('should create WIP commit before phase transitions', async () => {
135
+ const { GitManager } = await import('@codemcp/workflows-core');
136
+ vi.mocked(GitManager.isGitRepository).mockReturnValue(true);
137
+ vi.mocked(GitManager.hasUncommittedChanges).mockReturnValue(true);
138
+ vi.mocked(GitManager.createCommit).mockReturnValue(true);
139
+
140
+ const context: PluginHookContext = {
141
+ conversationId: 'test-conv',
142
+ planFilePath: '/test/plan.md',
143
+ currentPhase: 'explore',
144
+ workflow: 'epcc',
145
+ projectPath,
146
+ gitBranch: 'feature/test',
147
+ targetPhase: 'plan',
148
+ };
149
+
150
+ const hooks = plugin.getHooks();
151
+ await hooks.beforePhaseTransition?.(context, 'explore', 'plan');
152
+
153
+ expect(GitManager.hasUncommittedChanges).toHaveBeenCalledWith(
154
+ projectPath
155
+ );
156
+ expect(GitManager.createCommit).toHaveBeenCalledWith(
157
+ 'WIP: transition to plan',
158
+ projectPath
159
+ );
160
+ });
161
+ });
162
+
163
+ describe('End Commit Behavior', () => {
164
+ beforeEach(() => {
165
+ process.env.COMMIT_BEHAVIOR = 'end';
166
+ plugin = new CommitPlugin({ projectPath });
167
+ });
168
+
169
+ it('should add final commit task to plan file', async () => {
170
+ const context: PluginHookContext = {
171
+ conversationId: 'test-conv',
172
+ planFilePath: '/test/plan.md',
173
+ currentPhase: 'explore',
174
+ workflow: 'epcc',
175
+ projectPath,
176
+ gitBranch: 'feature/test',
177
+ };
178
+
179
+ const planContent = `## Commit
180
+ ### Tasks
181
+ - [ ] Review implementation
182
+ ### Completed
183
+ *None yet*`;
184
+
185
+ const hooks = plugin.getHooks();
186
+ const result = await hooks.afterPlanFileCreated?.(
187
+ context,
188
+ '/test/plan.md',
189
+ planContent
190
+ );
191
+
192
+ expect(result).toContain('Create a conventional commit');
193
+ expect(result).toContain('summarize the intentions and key decisions');
194
+ });
195
+ });
196
+ });
@@ -7,17 +7,23 @@ import { initializeServerComponents } from '../../src/server-config.js';
7
7
  import { mkdtemp, rm } from 'node:fs/promises';
8
8
  import { tmpdir } from 'node:os';
9
9
  import { join } from 'node:path';
10
+ import { execSync } from 'node:child_process';
11
+
12
+ // Mock child_process to control bd command availability
13
+ vi.mock('node:child_process', () => ({
14
+ execSync: vi.fn(),
15
+ }));
10
16
 
11
17
  describe('Server Config Plugin Registration', () => {
12
18
  let tempDir: string;
13
19
 
14
20
  beforeEach(async () => {
15
- vi.clearAllMocks();
21
+ vi.resetAllMocks(); // Reset mock implementations, not just call history
16
22
  tempDir = await mkdtemp(join(tmpdir(), 'server-config-test-'));
17
23
  });
18
24
 
19
25
  afterEach(async () => {
20
- vi.clearAllMocks();
26
+ vi.resetAllMocks();
21
27
  try {
22
28
  await rm(tempDir, { recursive: true, force: true });
23
29
  } catch {
@@ -25,9 +31,12 @@ describe('Server Config Plugin Registration', () => {
25
31
  }
26
32
  });
27
33
 
28
- it('should register BeadsPlugin when TASK_BACKEND is beads', async () => {
34
+ it('should register BeadsPlugin when TASK_BACKEND is beads and bd is available', async () => {
29
35
  vi.stubEnv('TASK_BACKEND', 'beads');
30
36
 
37
+ // Mock bd --version to return success
38
+ vi.mocked(execSync).mockReturnValue('beads v1.0.0\n');
39
+
31
40
  const components = await initializeServerComponents({
32
41
  projectPath: tempDir,
33
42
  });
@@ -45,8 +54,9 @@ describe('Server Config Plugin Registration', () => {
45
54
  expect(enabledPlugins[0].getName()).toBe('BeadsPlugin');
46
55
  });
47
56
 
48
- it('should not register BeadsPlugin when TASK_BACKEND is not beads', async () => {
49
- vi.stubEnv('TASK_BACKEND', 'none');
57
+ it('should not register BeadsPlugin when TASK_BACKEND is markdown', async () => {
58
+ // Explicitly set markdown to disable beads
59
+ vi.stubEnv('TASK_BACKEND', 'markdown');
50
60
 
51
61
  const components = await initializeServerComponents({
52
62
  projectPath: tempDir,
@@ -64,9 +74,14 @@ describe('Server Config Plugin Registration', () => {
64
74
  expect(enabledPlugins).toHaveLength(0);
65
75
  });
66
76
 
67
- it('should initialize empty plugin registry by default', async () => {
68
- // Don't set TASK_BACKEND environment variable
69
- vi.unstubAllEnvs();
77
+ it('should not register BeadsPlugin when bd is not available', async () => {
78
+ // Explicitly clear TASK_BACKEND - triggers auto-detection
79
+ delete process.env.TASK_BACKEND;
80
+
81
+ // Mock bd --version to throw (command not found)
82
+ vi.mocked(execSync).mockImplementation(() => {
83
+ throw new Error('command not found: bd');
84
+ });
70
85
 
71
86
  const components = await initializeServerComponents({
72
87
  projectPath: tempDir,
@@ -78,4 +93,7 @@ describe('Server Config Plugin Registration', () => {
78
93
  0
79
94
  );
80
95
  });
96
+
97
+ // Note: Auto-detection tests are covered in E2E tests (beads-plugin-integration.test.ts)
98
+ // because mocking child_process across package boundaries requires E2E-style server setup
81
99
  });