@codemcp/workflows 4.10.12 → 4.11.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 +14 -9
- 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 +15 -10
- 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/commit-plugin-integration.test.ts +222 -0
- package/test/unit/commit-plugin.test.ts +196 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/test/unit/commit-behaviour-interface.test.ts +0 -244
|
@@ -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
|
+
});
|