@codemcp/workflows-core 3.1.16

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 (114) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +674 -0
  3. package/dist/config-manager.d.ts +24 -0
  4. package/dist/config-manager.js +68 -0
  5. package/dist/config-manager.js.map +1 -0
  6. package/dist/conversation-manager.d.ts +97 -0
  7. package/dist/conversation-manager.js +367 -0
  8. package/dist/conversation-manager.js.map +1 -0
  9. package/dist/database.d.ts +73 -0
  10. package/dist/database.js +500 -0
  11. package/dist/database.js.map +1 -0
  12. package/dist/file-detection-manager.d.ts +53 -0
  13. package/dist/file-detection-manager.js +221 -0
  14. package/dist/file-detection-manager.js.map +1 -0
  15. package/dist/git-manager.d.ts +14 -0
  16. package/dist/git-manager.js +59 -0
  17. package/dist/git-manager.js.map +1 -0
  18. package/dist/index.d.ts +19 -0
  19. package/dist/index.js +25 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/instruction-generator.d.ts +69 -0
  22. package/dist/instruction-generator.js +133 -0
  23. package/dist/instruction-generator.js.map +1 -0
  24. package/dist/interaction-logger.d.ts +37 -0
  25. package/dist/interaction-logger.js +87 -0
  26. package/dist/interaction-logger.js.map +1 -0
  27. package/dist/logger.d.ts +64 -0
  28. package/dist/logger.js +283 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/path-validation-utils.d.ts +51 -0
  31. package/dist/path-validation-utils.js +202 -0
  32. package/dist/path-validation-utils.js.map +1 -0
  33. package/dist/plan-manager.d.ts +65 -0
  34. package/dist/plan-manager.js +256 -0
  35. package/dist/plan-manager.js.map +1 -0
  36. package/dist/project-docs-manager.d.ts +119 -0
  37. package/dist/project-docs-manager.js +357 -0
  38. package/dist/project-docs-manager.js.map +1 -0
  39. package/dist/state-machine-loader.d.ts +60 -0
  40. package/dist/state-machine-loader.js +235 -0
  41. package/dist/state-machine-loader.js.map +1 -0
  42. package/dist/state-machine-types.d.ts +58 -0
  43. package/dist/state-machine-types.js +7 -0
  44. package/dist/state-machine-types.js.map +1 -0
  45. package/dist/state-machine.d.ts +52 -0
  46. package/dist/state-machine.js +256 -0
  47. package/dist/state-machine.js.map +1 -0
  48. package/dist/system-prompt-generator.d.ts +17 -0
  49. package/dist/system-prompt-generator.js +113 -0
  50. package/dist/system-prompt-generator.js.map +1 -0
  51. package/dist/template-manager.d.ts +61 -0
  52. package/dist/template-manager.js +229 -0
  53. package/dist/template-manager.js.map +1 -0
  54. package/dist/transition-engine.d.ts +70 -0
  55. package/dist/transition-engine.js +240 -0
  56. package/dist/transition-engine.js.map +1 -0
  57. package/dist/types.d.ts +56 -0
  58. package/dist/types.js +5 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/workflow-manager.d.ts +89 -0
  61. package/dist/workflow-manager.js +466 -0
  62. package/dist/workflow-manager.js.map +1 -0
  63. package/package.json +27 -0
  64. package/src/config-manager.ts +96 -0
  65. package/src/conversation-manager.ts +492 -0
  66. package/src/database.ts +685 -0
  67. package/src/file-detection-manager.ts +302 -0
  68. package/src/git-manager.ts +64 -0
  69. package/src/index.ts +28 -0
  70. package/src/instruction-generator.ts +210 -0
  71. package/src/interaction-logger.ts +109 -0
  72. package/src/logger.ts +353 -0
  73. package/src/path-validation-utils.ts +261 -0
  74. package/src/plan-manager.ts +323 -0
  75. package/src/project-docs-manager.ts +522 -0
  76. package/src/state-machine-loader.ts +308 -0
  77. package/src/state-machine-types.ts +72 -0
  78. package/src/state-machine.ts +370 -0
  79. package/src/system-prompt-generator.ts +122 -0
  80. package/src/template-manager.ts +321 -0
  81. package/src/transition-engine.ts +386 -0
  82. package/src/types.ts +60 -0
  83. package/src/workflow-manager.ts +601 -0
  84. package/test/unit/conversation-manager.test.ts +179 -0
  85. package/test/unit/custom-workflow-loading.test.ts +174 -0
  86. package/test/unit/directory-linking-and-extensions.test.ts +338 -0
  87. package/test/unit/file-linking-integration.test.ts +256 -0
  88. package/test/unit/git-commit-integration.test.ts +91 -0
  89. package/test/unit/git-manager.test.ts +86 -0
  90. package/test/unit/install-workflow.test.ts +138 -0
  91. package/test/unit/instruction-generator.test.ts +247 -0
  92. package/test/unit/list-workflows-filtering.test.ts +68 -0
  93. package/test/unit/none-template-functionality.test.ts +224 -0
  94. package/test/unit/project-docs-manager.test.ts +337 -0
  95. package/test/unit/state-machine-loader.test.ts +234 -0
  96. package/test/unit/template-manager.test.ts +217 -0
  97. package/test/unit/validate-workflow-name.test.ts +150 -0
  98. package/test/unit/workflow-domain-filtering.test.ts +75 -0
  99. package/test/unit/workflow-enum-generation.test.ts +92 -0
  100. package/test/unit/workflow-manager-enhanced-path-resolution.test.ts +369 -0
  101. package/test/unit/workflow-manager-path-resolution.test.ts +150 -0
  102. package/test/unit/workflow-migration.test.ts +155 -0
  103. package/test/unit/workflow-override-by-name.test.ts +116 -0
  104. package/test/unit/workflow-prioritization.test.ts +38 -0
  105. package/test/unit/workflow-validation.test.ts +303 -0
  106. package/test/utils/e2e-test-setup.ts +453 -0
  107. package/test/utils/run-server-in-dir.sh +27 -0
  108. package/test/utils/temp-files.ts +308 -0
  109. package/test/utils/test-access.ts +79 -0
  110. package/test/utils/test-helpers.ts +286 -0
  111. package/test/utils/test-setup.ts +78 -0
  112. package/tsconfig.build.json +21 -0
  113. package/tsconfig.json +8 -0
  114. package/vitest.config.ts +18 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Unit tests for ProjectDocsManager
3
+ *
4
+ * Tests project documentation management functionality
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
+ import { TestAccess } from '../utils/test-access.js';
9
+ import { ProjectDocsManager } from '@codemcp/workflows-core';
10
+ import { TemplateManager } from '@codemcp/workflows-core';
11
+ import { mkdir, writeFile, rmdir, readFile } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { tmpdir } from 'node:os';
14
+
15
+ // Mock TemplateManager
16
+ vi.mock('../../src/template-manager.js');
17
+
18
+ describe('ProjectDocsManager', () => {
19
+ let projectDocsManager: ProjectDocsManager;
20
+ let testProjectPath: string;
21
+ let mockTemplateManager: vi.Mocked<TemplateManager>;
22
+
23
+ beforeEach(async () => {
24
+ // Create a temporary directory for test project
25
+ testProjectPath = join(tmpdir(), `project-test-${Date.now()}`);
26
+ await mkdir(testProjectPath, { recursive: true });
27
+
28
+ // Mock TemplateManager
29
+ mockTemplateManager = {
30
+ getDefaults: vi.fn().mockReturnValue({
31
+ architecture: 'arc42',
32
+ requirements: 'ears',
33
+ design: 'comprehensive',
34
+ }),
35
+ validateOptions: vi.fn(),
36
+ loadTemplate: vi.fn(),
37
+ getAvailableTemplates: vi.fn(),
38
+ } as Partial<TemplateManager>;
39
+
40
+ projectDocsManager = new ProjectDocsManager();
41
+ TestAccess.injectMock(
42
+ projectDocsManager,
43
+ 'templateManager',
44
+ mockTemplateManager
45
+ );
46
+ });
47
+
48
+ afterEach(async () => {
49
+ // Clean up test directory
50
+ try {
51
+ await rmdir(testProjectPath, { recursive: true });
52
+ } catch {
53
+ // Ignore cleanup errors
54
+ }
55
+ });
56
+
57
+ describe('getDocsPath', () => {
58
+ it('should return correct docs path', () => {
59
+ const docsPath = projectDocsManager.getDocsPath(testProjectPath);
60
+ expect(docsPath).toBe(join(testProjectPath, '.vibe', 'docs'));
61
+ });
62
+ });
63
+
64
+ describe('getDocumentPaths', () => {
65
+ it('should return correct document paths', () => {
66
+ const paths = projectDocsManager.getDocumentPaths(testProjectPath);
67
+
68
+ expect(paths.architecture).toBe(
69
+ join(testProjectPath, '.vibe', 'docs', 'architecture.md')
70
+ );
71
+ expect(paths.requirements).toBe(
72
+ join(testProjectPath, '.vibe', 'docs', 'requirements.md')
73
+ );
74
+ expect(paths.design).toBe(
75
+ join(testProjectPath, '.vibe', 'docs', 'design.md')
76
+ );
77
+ });
78
+ });
79
+
80
+ describe('getProjectDocsInfo', () => {
81
+ it('should return info when no documents exist', async () => {
82
+ const info = await projectDocsManager.getProjectDocsInfo(testProjectPath);
83
+
84
+ expect(info.architecture.exists).toBe(false);
85
+ expect(info.requirements.exists).toBe(false);
86
+ expect(info.design.exists).toBe(false);
87
+ expect(info.architecture.path).toBe(
88
+ join(testProjectPath, '.vibe', 'docs', 'architecture.md')
89
+ );
90
+ });
91
+
92
+ it('should return info when documents exist', async () => {
93
+ // Create docs directory and files
94
+ const docsPath = join(testProjectPath, '.vibe', 'docs');
95
+ await mkdir(docsPath, { recursive: true });
96
+ await writeFile(join(docsPath, 'architecture.md'), '# Architecture');
97
+ await writeFile(join(docsPath, 'requirements.md'), '# Requirements');
98
+
99
+ const info = await projectDocsManager.getProjectDocsInfo(testProjectPath);
100
+
101
+ expect(info.architecture.exists).toBe(true);
102
+ expect(info.requirements.exists).toBe(true);
103
+ expect(info.design.exists).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe('createProjectDocs', () => {
108
+ beforeEach(() => {
109
+ // Setup template manager mocks
110
+ mockTemplateManager.loadTemplate.mockImplementation(
111
+ async (type, template) => {
112
+ return {
113
+ content: `# Test ${type} template (${template})\n\nTest content for ${type}.`,
114
+ };
115
+ }
116
+ );
117
+ });
118
+
119
+ it('should create all documents with default templates', async () => {
120
+ const result =
121
+ await projectDocsManager.createProjectDocs(testProjectPath);
122
+
123
+ expect(result.created).toEqual([
124
+ 'architecture.md',
125
+ 'requirements.md',
126
+ 'design.md',
127
+ ]);
128
+ expect(result.skipped).toEqual([]);
129
+
130
+ // Verify template manager was called with defaults
131
+ expect(mockTemplateManager.loadTemplate).toHaveBeenCalledWith(
132
+ 'architecture',
133
+ 'arc42'
134
+ );
135
+ expect(mockTemplateManager.loadTemplate).toHaveBeenCalledWith(
136
+ 'requirements',
137
+ 'ears'
138
+ );
139
+ expect(mockTemplateManager.loadTemplate).toHaveBeenCalledWith(
140
+ 'design',
141
+ 'comprehensive'
142
+ );
143
+
144
+ // Verify files were created
145
+ const docsPath = join(testProjectPath, '.vibe', 'docs');
146
+ const archContent = await readFile(
147
+ join(docsPath, 'architecture.md'),
148
+ 'utf-8'
149
+ );
150
+ expect(archContent).toContain('Test architecture template (arc42)');
151
+ });
152
+
153
+ it('should create documents with custom templates', async () => {
154
+ const options = {
155
+ architecture: 'freestyle' as const,
156
+ requirements: 'freestyle' as const,
157
+ design: 'freestyle' as const,
158
+ };
159
+
160
+ const result = await projectDocsManager.createProjectDocs(
161
+ testProjectPath,
162
+ options
163
+ );
164
+
165
+ expect(result.created).toEqual([
166
+ 'architecture.md',
167
+ 'requirements.md',
168
+ 'design.md',
169
+ ]);
170
+ expect(result.skipped).toEqual([]);
171
+
172
+ // Verify template manager was called with custom options
173
+ expect(mockTemplateManager.loadTemplate).toHaveBeenCalledWith(
174
+ 'architecture',
175
+ 'freestyle'
176
+ );
177
+ expect(mockTemplateManager.loadTemplate).toHaveBeenCalledWith(
178
+ 'requirements',
179
+ 'freestyle'
180
+ );
181
+ expect(mockTemplateManager.loadTemplate).toHaveBeenCalledWith(
182
+ 'design',
183
+ 'freestyle'
184
+ );
185
+ });
186
+
187
+ it('should skip existing documents', async () => {
188
+ // Create docs directory and one existing file
189
+ const docsPath = join(testProjectPath, '.vibe', 'docs');
190
+ await mkdir(docsPath, { recursive: true });
191
+ await writeFile(
192
+ join(docsPath, 'architecture.md'),
193
+ '# Existing Architecture'
194
+ );
195
+
196
+ const result =
197
+ await projectDocsManager.createProjectDocs(testProjectPath);
198
+
199
+ expect(result.created).toEqual(['requirements.md', 'design.md']);
200
+ expect(result.skipped).toEqual(['architecture.md']);
201
+
202
+ // Verify existing file wasn't overwritten
203
+ const archContent = await readFile(
204
+ join(docsPath, 'architecture.md'),
205
+ 'utf-8'
206
+ );
207
+ expect(archContent).toBe('# Existing Architecture');
208
+ });
209
+
210
+ it('should handle template with additional files', async () => {
211
+ mockTemplateManager.loadTemplate.mockImplementation(
212
+ async (type, template) => {
213
+ if (type === 'architecture' && template === 'arc42') {
214
+ return {
215
+ content: '# Arc42 Template\n\nSee images/diagram.png',
216
+ additionalFiles: [
217
+ {
218
+ relativePath: 'images/diagram.png',
219
+ content: Buffer.from('fake-image-data'),
220
+ },
221
+ ],
222
+ };
223
+ }
224
+ return {
225
+ content: `# Test ${type} template (${template})`,
226
+ };
227
+ }
228
+ );
229
+
230
+ const result =
231
+ await projectDocsManager.createProjectDocs(testProjectPath);
232
+
233
+ expect(result.created).toEqual([
234
+ 'architecture.md',
235
+ 'requirements.md',
236
+ 'design.md',
237
+ ]);
238
+
239
+ // Verify additional file was created
240
+ const imagePath = join(
241
+ testProjectPath,
242
+ '.vibe',
243
+ 'docs',
244
+ 'images',
245
+ 'diagram.png'
246
+ );
247
+ const imageContent = await readFile(imagePath);
248
+ expect(imageContent).toEqual(Buffer.from('fake-image-data'));
249
+ });
250
+
251
+ it('should handle template loading errors', async () => {
252
+ mockTemplateManager.loadTemplate.mockRejectedValue(
253
+ new Error('Template not found')
254
+ );
255
+
256
+ // The method should throw when template loading fails
257
+ await expect(
258
+ projectDocsManager.createProjectDocs(testProjectPath)
259
+ ).rejects.toThrow('Template not found');
260
+ });
261
+ });
262
+
263
+ describe('getVariableSubstitutions', () => {
264
+ it('should return correct variable substitutions', () => {
265
+ const substitutions =
266
+ projectDocsManager.getVariableSubstitutions(testProjectPath);
267
+
268
+ expect(substitutions).toEqual({
269
+ $ARCHITECTURE_DOC: join(
270
+ testProjectPath,
271
+ '.vibe',
272
+ 'docs',
273
+ 'architecture.md'
274
+ ),
275
+ $REQUIREMENTS_DOC: join(
276
+ testProjectPath,
277
+ '.vibe',
278
+ 'docs',
279
+ 'requirements.md'
280
+ ),
281
+ $DESIGN_DOC: join(testProjectPath, '.vibe', 'docs', 'design.md'),
282
+ });
283
+ });
284
+ });
285
+
286
+ describe('readDocument', () => {
287
+ it('should return path to existing document', async () => {
288
+ // Create docs directory and file
289
+ const docsPath = join(testProjectPath, '.vibe', 'docs');
290
+ await mkdir(docsPath, { recursive: true });
291
+ await writeFile(join(docsPath, 'architecture.md'), '# Test Architecture');
292
+
293
+ const path = await projectDocsManager.readDocument(
294
+ testProjectPath,
295
+ 'architecture'
296
+ );
297
+ expect(path).toContain('architecture.md');
298
+ expect(path).toContain('.vibe/docs');
299
+ });
300
+
301
+ it('should throw error for non-existent document', async () => {
302
+ await expect(
303
+ projectDocsManager.readDocument(testProjectPath, 'architecture')
304
+ ).rejects.toThrow('architecture document not found');
305
+ });
306
+ });
307
+
308
+ describe('allDocumentsExist', () => {
309
+ it('should return false when no documents exist', async () => {
310
+ const result =
311
+ await projectDocsManager.allDocumentsExist(testProjectPath);
312
+ expect(result).toBe(false);
313
+ });
314
+
315
+ it('should return false when some documents exist', async () => {
316
+ const docsPath = join(testProjectPath, '.vibe', 'docs');
317
+ await mkdir(docsPath, { recursive: true });
318
+ await writeFile(join(docsPath, 'architecture.md'), '# Architecture');
319
+
320
+ const result =
321
+ await projectDocsManager.allDocumentsExist(testProjectPath);
322
+ expect(result).toBe(false);
323
+ });
324
+
325
+ it('should return true when all documents exist', async () => {
326
+ const docsPath = join(testProjectPath, '.vibe', 'docs');
327
+ await mkdir(docsPath, { recursive: true });
328
+ await writeFile(join(docsPath, 'architecture.md'), '# Architecture');
329
+ await writeFile(join(docsPath, 'requirements.md'), '# Requirements');
330
+ await writeFile(join(docsPath, 'design.md'), '# Design');
331
+
332
+ const result =
333
+ await projectDocsManager.allDocumentsExist(testProjectPath);
334
+ expect(result).toBe(true);
335
+ });
336
+ });
337
+ });
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Unit tests for StateMachineLoader
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { StateMachineLoader } from '@codemcp/workflows-core';
7
+
8
+ // Mock global URL constructor
9
+ global.URL = vi.fn().mockImplementation(() => ({
10
+ pathname: '/mock/src/state-machine-loader.ts',
11
+ })) as typeof URL;
12
+
13
+ // Mock import.meta.url
14
+ vi.stubGlobal('import.meta', {
15
+ url: 'file:///mock/src/state-machine-loader.ts',
16
+ });
17
+
18
+ // Create a mock state machine object
19
+ const mockStateMachine = {
20
+ name: 'Test State Machine',
21
+ description: 'Test state machine for unit tests',
22
+ initial_state: 'idle',
23
+ states: {
24
+ idle: {
25
+ description: 'Idle state',
26
+ default_instructions:
27
+ 'Starting idle state. Wait for user input and analyze requests.',
28
+ transitions: [
29
+ {
30
+ trigger: 'new_feature_request',
31
+ to: 'requirements',
32
+ instructions: 'Start requirements analysis',
33
+ transition_reason: 'New feature request detected',
34
+ },
35
+ ],
36
+ },
37
+ requirements: {
38
+ description: 'Requirements state',
39
+ default_instructions:
40
+ 'Starting requirements analysis. Gather and document user requirements.',
41
+ transitions: [],
42
+ },
43
+ },
44
+ };
45
+
46
+ // Mock modules
47
+ vi.mock('fs', () => {
48
+ return {
49
+ default: {
50
+ existsSync: vi.fn(),
51
+ readFileSync: vi.fn(),
52
+ },
53
+ existsSync: vi.fn(),
54
+ readFileSync: vi.fn(),
55
+ };
56
+ });
57
+
58
+ vi.mock('path', () => {
59
+ return {
60
+ default: {
61
+ resolve: vi.fn(p => p),
62
+ join: vi.fn((...paths) => paths.join('/')),
63
+ dirname: vi.fn(p => {
64
+ if (!p) return '';
65
+ const lastSlash = p.lastIndexOf('/');
66
+ return lastSlash >= 0 ? p.substring(0, lastSlash) : p;
67
+ }),
68
+ },
69
+ resolve: vi.fn(p => p),
70
+ join: vi.fn((...paths) => paths.join('/')),
71
+ dirname: vi.fn(p => {
72
+ if (!p) return '';
73
+ const lastSlash = p.lastIndexOf('/');
74
+ return lastSlash >= 0 ? p.substring(0, lastSlash) : p;
75
+ }),
76
+ };
77
+ });
78
+
79
+ // Properly mock js-yaml with a default export
80
+ vi.mock('js-yaml', () => {
81
+ return {
82
+ default: {
83
+ load: vi.fn(() => mockStateMachine),
84
+ dump: vi.fn(() => 'mocked yaml content'),
85
+ },
86
+ load: vi.fn(() => mockStateMachine),
87
+ dump: vi.fn(() => 'mocked yaml content'),
88
+ };
89
+ });
90
+
91
+ // Silence logger
92
+ vi.mock('../../src/logger.ts', () => ({
93
+ createLogger: () => ({
94
+ debug: vi.fn(),
95
+ info: vi.fn(),
96
+ warn: vi.fn(),
97
+ error: vi.fn(),
98
+ child: () => ({
99
+ debug: vi.fn(),
100
+ info: vi.fn(),
101
+ warn: vi.fn(),
102
+ error: vi.fn(),
103
+ }),
104
+ }),
105
+ }));
106
+
107
+ // Import mocked modules
108
+ import fs from 'node:fs';
109
+
110
+ describe('StateMachineLoader', () => {
111
+ let stateMachineLoader: StateMachineLoader;
112
+
113
+ beforeEach(() => {
114
+ // Reset mocks
115
+ vi.resetAllMocks();
116
+
117
+ // Create a new instance for each test
118
+ stateMachineLoader = new StateMachineLoader();
119
+ });
120
+
121
+ afterEach(() => {
122
+ vi.resetAllMocks();
123
+ });
124
+
125
+ describe('loadStateMachine', () => {
126
+ it('should load custom state machine file if it exists', () => {
127
+ // Mock fs.existsSync to return true for custom file
128
+ vi.mocked(fs.existsSync).mockImplementation(p => {
129
+ return String(p) === 'project/.vibe/workflow.yaml';
130
+ });
131
+
132
+ // Mock fs.readFileSync to return valid YAML content
133
+ vi.mocked(fs.readFileSync).mockReturnValue('valid yaml content');
134
+
135
+ const result = stateMachineLoader.loadStateMachine('project');
136
+
137
+ expect(fs.existsSync).toHaveBeenCalledWith('project/.vibe/workflow.yaml');
138
+ expect(fs.readFileSync).toHaveBeenCalledWith(
139
+ 'project/.vibe/workflow.yaml',
140
+ 'utf8'
141
+ );
142
+ expect(result).toBeDefined();
143
+ expect(result.name).toBe('Test State Machine');
144
+ });
145
+
146
+ it('should load default state machine file if custom file does not exist', () => {
147
+ // Mock fs.existsSync to return false for all paths
148
+ vi.mocked(fs.existsSync).mockReturnValue(false);
149
+
150
+ // Mock fs.readFileSync to return valid YAML content
151
+ vi.mocked(fs.readFileSync).mockReturnValue('valid yaml content');
152
+
153
+ const result = stateMachineLoader.loadStateMachine('project');
154
+
155
+ expect(fs.existsSync).toHaveBeenCalledWith('project/.vibe/workflow.yaml');
156
+ expect(fs.existsSync).toHaveBeenCalledWith('project/.vibe/workflow.yml');
157
+
158
+ // The path might be absolute or relative depending on the environment
159
+ expect(fs.readFileSync).toHaveBeenCalled();
160
+ expect(vi.mocked(fs.readFileSync).mock.calls[0][0]).toMatch(
161
+ /.*resources\/workflows\/waterfall\.yaml$/
162
+ );
163
+ expect(vi.mocked(fs.readFileSync).mock.calls[0][1]).toBe('utf8');
164
+ expect(result).toBeDefined();
165
+ expect(result.name).toBe('Test State Machine');
166
+ });
167
+ });
168
+
169
+ describe('loadFromFile', () => {
170
+ it('should load and validate state machine from file', () => {
171
+ // Mock fs.readFileSync to return valid YAML content
172
+ vi.mocked(fs.readFileSync).mockReturnValue('valid yaml content');
173
+
174
+ const result = stateMachineLoader.loadFromFile('test-file.yaml');
175
+
176
+ expect(fs.readFileSync).toHaveBeenCalledWith('test-file.yaml', 'utf8');
177
+ expect(result).toBeDefined();
178
+ expect(result.name).toBe('Test State Machine');
179
+ });
180
+
181
+ it('should throw error if file cannot be loaded', () => {
182
+ // Mock fs.readFileSync to throw error
183
+ vi.mocked(fs.readFileSync).mockImplementation(() => {
184
+ throw new Error('File not found');
185
+ });
186
+
187
+ expect(() =>
188
+ stateMachineLoader.loadFromFile('invalid-file.yaml')
189
+ ).toThrow('Failed to load state machine: File not found');
190
+ });
191
+ });
192
+
193
+ describe('getTransitionInstructions', () => {
194
+ beforeEach(() => {
195
+ // Load state machine
196
+ vi.mocked(fs.readFileSync).mockReturnValue('valid yaml content');
197
+ stateMachineLoader.loadFromFile('test-file.yaml');
198
+ });
199
+
200
+ it('should return transition instructions for modeled transition', () => {
201
+ const result = stateMachineLoader.getTransitionInstructions(
202
+ 'idle',
203
+ 'requirements',
204
+ 'new_feature_request'
205
+ );
206
+
207
+ expect(result).toEqual({
208
+ instructions: 'Start requirements analysis',
209
+ transitionReason: 'New feature request detected',
210
+ isModeled: true,
211
+ });
212
+ });
213
+
214
+ it('should return default instructions when no modeled transition exists', () => {
215
+ const result = stateMachineLoader.getTransitionInstructions(
216
+ 'requirements',
217
+ 'idle'
218
+ );
219
+
220
+ expect(result).toEqual({
221
+ instructions:
222
+ 'Starting idle state. Wait for user input and analyze requests.',
223
+ transitionReason: 'Direct transition to idle phase',
224
+ isModeled: false,
225
+ });
226
+ });
227
+
228
+ it('should throw error if no transition found', () => {
229
+ expect(() =>
230
+ stateMachineLoader.getTransitionInstructions('requirements', 'unknown')
231
+ ).toThrow('Target state "unknown" not found');
232
+ });
233
+ });
234
+ });