@codemcp/workflows 4.10.11 → 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.
Files changed (31) 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 +14 -9
  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 +15 -10
  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/commit-plugin-integration.test.ts +222 -0
  29. package/test/unit/commit-plugin.test.ts +196 -0
  30. package/tsconfig.build.tsbuildinfo +1 -1
  31. 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
+ });