@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/plugin-system/commit-plugin.d.ts +40 -0
- package/dist/plugin-system/commit-plugin.d.ts.map +1 -0
- package/dist/plugin-system/commit-plugin.js +204 -0
- package/dist/plugin-system/commit-plugin.js.map +1 -0
- package/dist/plugin-system/plugin-interfaces.d.ts +0 -1
- package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -1
- package/dist/server-config.d.ts.map +1 -1
- package/dist/server-config.js +28 -15
- package/dist/server-config.js.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.js +2 -9
- package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
- package/dist/tool-handlers/start-development.d.ts +0 -1
- package/dist/tool-handlers/start-development.d.ts.map +1 -1
- package/dist/tool-handlers/start-development.js +25 -22
- package/dist/tool-handlers/start-development.js.map +1 -1
- package/dist/tool-handlers/whats-next.d.ts.map +1 -1
- package/dist/tool-handlers/whats-next.js +2 -8
- package/dist/tool-handlers/whats-next.js.map +1 -1
- package/package.json +2 -2
- package/src/plugin-system/commit-plugin.ts +252 -0
- package/src/plugin-system/plugin-interfaces.ts +0 -1
- package/src/server-config.ts +36 -16
- package/src/tool-handlers/proceed-to-phase.ts +2 -12
- package/src/tool-handlers/start-development.ts +39 -29
- package/src/tool-handlers/whats-next.ts +2 -11
- package/test/e2e/beads-plugin-integration.test.ts +41 -12
- package/test/e2e/commit-plugin-integration.test.ts +222 -0
- package/test/unit/commit-plugin.test.ts +196 -0
- package/test/unit/server-config-plugin-registry.test.ts +26 -8
- package/tsconfig.build.tsbuildinfo +1 -1
- 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
|
-
//
|
|
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:
|
|
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
|
|
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-
|
|
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
|
|
886
|
-
expect(planContent).
|
|
888
|
+
// VALIDATE: Beads features auto-detected and enabled
|
|
889
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
887
890
|
});
|
|
888
891
|
|
|
889
|
-
it('should
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
49
|
-
|
|
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
|
|
68
|
-
//
|
|
69
|
-
|
|
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
|
});
|