@codemcp/workflows-core 3.1.21 → 3.2.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 (80) hide show
  1. package/package.json +9 -5
  2. package/resources/templates/architecture/arc42/arc42-template-EN.md +1077 -0
  3. package/resources/templates/architecture/arc42/images/01_2_iso-25010-topics-EN.drawio-2023.png +0 -0
  4. package/resources/templates/architecture/arc42/images/01_2_iso-25010-topics-EN.drawio.png +0 -0
  5. package/resources/templates/architecture/arc42/images/05_building_blocks-EN.png +0 -0
  6. package/resources/templates/architecture/arc42/images/08-concepts-EN.drawio.png +0 -0
  7. package/resources/templates/architecture/arc42/images/arc42-logo.png +0 -0
  8. package/resources/templates/architecture/c4.md +224 -0
  9. package/resources/templates/architecture/freestyle.md +53 -0
  10. package/resources/templates/architecture/none.md +17 -0
  11. package/resources/templates/design/comprehensive.md +207 -0
  12. package/resources/templates/design/freestyle.md +37 -0
  13. package/resources/templates/design/none.md +17 -0
  14. package/resources/templates/requirements/ears.md +90 -0
  15. package/resources/templates/requirements/freestyle.md +42 -0
  16. package/resources/templates/requirements/none.md +17 -0
  17. package/resources/workflows/big-bang-conversion.yaml +539 -0
  18. package/resources/workflows/boundary-testing.yaml +334 -0
  19. package/resources/workflows/bugfix.yaml +185 -0
  20. package/resources/workflows/business-analysis.yaml +671 -0
  21. package/resources/workflows/c4-analysis.yaml +485 -0
  22. package/resources/workflows/epcc.yaml +161 -0
  23. package/resources/workflows/greenfield.yaml +189 -0
  24. package/resources/workflows/minor.yaml +127 -0
  25. package/resources/workflows/posts.yaml +207 -0
  26. package/resources/workflows/slides.yaml +256 -0
  27. package/resources/workflows/tdd.yaml +157 -0
  28. package/resources/workflows/waterfall.yaml +195 -0
  29. package/.turbo/turbo-build.log +0 -4
  30. package/src/config-manager.ts +0 -96
  31. package/src/conversation-manager.ts +0 -489
  32. package/src/database.ts +0 -427
  33. package/src/file-detection-manager.ts +0 -302
  34. package/src/git-manager.ts +0 -64
  35. package/src/index.ts +0 -28
  36. package/src/instruction-generator.ts +0 -210
  37. package/src/interaction-logger.ts +0 -109
  38. package/src/logger.ts +0 -353
  39. package/src/path-validation-utils.ts +0 -261
  40. package/src/plan-manager.ts +0 -323
  41. package/src/project-docs-manager.ts +0 -523
  42. package/src/state-machine-loader.ts +0 -365
  43. package/src/state-machine-types.ts +0 -72
  44. package/src/state-machine.ts +0 -370
  45. package/src/system-prompt-generator.ts +0 -122
  46. package/src/template-manager.ts +0 -328
  47. package/src/transition-engine.ts +0 -386
  48. package/src/types.ts +0 -60
  49. package/src/workflow-manager.ts +0 -606
  50. package/test/unit/conversation-manager.test.ts +0 -179
  51. package/test/unit/custom-workflow-loading.test.ts +0 -174
  52. package/test/unit/directory-linking-and-extensions.test.ts +0 -338
  53. package/test/unit/file-linking-integration.test.ts +0 -256
  54. package/test/unit/git-commit-integration.test.ts +0 -91
  55. package/test/unit/git-manager.test.ts +0 -86
  56. package/test/unit/install-workflow.test.ts +0 -138
  57. package/test/unit/instruction-generator.test.ts +0 -247
  58. package/test/unit/list-workflows-filtering.test.ts +0 -68
  59. package/test/unit/none-template-functionality.test.ts +0 -224
  60. package/test/unit/project-docs-manager.test.ts +0 -337
  61. package/test/unit/state-machine-loader.test.ts +0 -234
  62. package/test/unit/template-manager.test.ts +0 -217
  63. package/test/unit/validate-workflow-name.test.ts +0 -150
  64. package/test/unit/workflow-domain-filtering.test.ts +0 -75
  65. package/test/unit/workflow-enum-generation.test.ts +0 -92
  66. package/test/unit/workflow-manager-enhanced-path-resolution.test.ts +0 -369
  67. package/test/unit/workflow-manager-path-resolution.test.ts +0 -150
  68. package/test/unit/workflow-migration.test.ts +0 -155
  69. package/test/unit/workflow-override-by-name.test.ts +0 -116
  70. package/test/unit/workflow-prioritization.test.ts +0 -38
  71. package/test/unit/workflow-validation.test.ts +0 -303
  72. package/test/utils/e2e-test-setup.ts +0 -453
  73. package/test/utils/run-server-in-dir.sh +0 -27
  74. package/test/utils/temp-files.ts +0 -308
  75. package/test/utils/test-access.ts +0 -79
  76. package/test/utils/test-helpers.ts +0 -286
  77. package/test/utils/test-setup.ts +0 -78
  78. package/tsconfig.build.json +0 -21
  79. package/tsconfig.json +0 -8
  80. package/vitest.config.ts +0 -18
@@ -1,179 +0,0 @@
1
- /**
2
- * Unit tests for ConversationManager
3
- */
4
-
5
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
- import { ConversationManager } from '../../src/conversation-manager.js';
7
- import type { Database } from '../../src/database.js';
8
-
9
- // Mock WorkflowManager state machine
10
- const mockStateMachine = {
11
- name: 'Test Workflow',
12
- description: 'Test workflow for unit tests',
13
- initial_state: 'idle',
14
- states: {
15
- idle: {
16
- description: 'Idle state',
17
- transitions: [],
18
- },
19
- },
20
- };
21
-
22
- describe('ConversationManager', () => {
23
- let conversationManager: ConversationManager;
24
-
25
- // Mock database functions
26
- const mockGetConversationState = vi.fn();
27
- const mockSaveConversationState = vi.fn();
28
- const mockDeleteConversationState = vi.fn();
29
- const mockSoftDeleteInteractionLogs = vi.fn();
30
- const mockInitialize = vi.fn();
31
- const mockClose = vi.fn();
32
-
33
- beforeEach(() => {
34
- vi.resetAllMocks();
35
-
36
- // Create mock WorkflowManager
37
- const mockWorkflowManager = {
38
- loadWorkflowForProject: vi.fn().mockReturnValue(mockStateMachine),
39
- validateWorkflowName: vi.fn().mockReturnValue(true),
40
- getWorkflowNames: vi
41
- .fn()
42
- .mockReturnValue([
43
- 'mock-workflow-1',
44
- 'mock-workflow-2',
45
- 'mock-workflow-3',
46
- ]),
47
- };
48
-
49
- // Create a mock database with the mocked functions
50
- const mockDb = {
51
- getConversationState: mockGetConversationState,
52
- saveConversationState: mockSaveConversationState,
53
- deleteConversationState: mockDeleteConversationState,
54
- softDeleteInteractionLogs: mockSoftDeleteInteractionLogs,
55
- initialize: mockInitialize,
56
- close: mockClose,
57
- };
58
-
59
- // Create conversation manager with dependency injection
60
- conversationManager = new ConversationManager(
61
- mockDb as Database,
62
- mockWorkflowManager,
63
- '/test/project/path'
64
- );
65
- });
66
-
67
- afterEach(() => {
68
- vi.resetAllMocks();
69
- });
70
-
71
- describe('getConversationContext', () => {
72
- it('should throw an error when no conversation exists', async () => {
73
- // Mock database to return null (no conversation)
74
- mockGetConversationState.mockResolvedValue(null);
75
-
76
- // Expect the method to throw an error
77
- await expect(
78
- conversationManager.getConversationContext()
79
- ).rejects.toThrow(
80
- 'No development conversation exists for this project. Use the start_development tool first to initialize development with a workflow.'
81
- );
82
-
83
- // Verify database was called with expected ID
84
- expect(mockGetConversationState).toHaveBeenCalledWith(expect.any(String));
85
- });
86
-
87
- it('should return conversation context when conversation exists', async () => {
88
- // Mock existing conversation state
89
- const mockState = {
90
- conversationId: 'test-conversation-id',
91
- projectPath: '/test/project/path',
92
- gitBranch: 'main',
93
- currentPhase: 'idle',
94
- planFilePath: '/test/project/path/.vibe/development-plan.md',
95
- workflowName: 'custom',
96
- createdAt: '2025-06-25T00:00:00.000Z',
97
- updatedAt: '2025-06-25T00:00:00.000Z',
98
- };
99
-
100
- mockGetConversationState.mockResolvedValue(mockState);
101
-
102
- // Call the method
103
- const result = await conversationManager.getConversationContext();
104
-
105
- // Verify result
106
- expect(result).toEqual({
107
- conversationId: 'test-conversation-id',
108
- projectPath: '/test/project/path',
109
- gitBranch: 'main',
110
- currentPhase: 'idle',
111
- planFilePath: '/test/project/path/.vibe/development-plan.md',
112
- workflowName: 'custom',
113
- });
114
-
115
- // Verify database was called with expected ID
116
- expect(mockGetConversationState).toHaveBeenCalledWith(expect.any(String));
117
- });
118
- });
119
-
120
- describe('createConversationContext', () => {
121
- it('should create a new conversation when none exists', async () => {
122
- // Mock database to return null (no conversation) then accept the new state
123
- mockGetConversationState.mockResolvedValue(null);
124
- mockSaveConversationState.mockResolvedValue(undefined);
125
-
126
- // Call the method
127
- const result =
128
- await conversationManager.createConversationContext('mock-workflow');
129
-
130
- // Verify result has expected structure
131
- expect(result).toHaveProperty('conversationId');
132
- expect(result).toHaveProperty('projectPath', '/test/project/path');
133
- expect(result).toHaveProperty('gitBranch', 'default');
134
- expect(result).toHaveProperty('currentPhase', 'idle'); // Should be idle from mock state machine
135
- expect(result).toHaveProperty('planFilePath');
136
- expect(result).toHaveProperty('workflowName', 'mock-workflow');
137
-
138
- // Verify database was called to save the new state
139
- expect(mockSaveConversationState).toHaveBeenCalled();
140
- const savedState = mockSaveConversationState.mock.calls[0][0];
141
- expect(savedState.workflowName).toBe('mock-workflow'); // Should match what we passed in
142
- expect(savedState.currentPhase).toBe('idle');
143
- });
144
-
145
- it('should return existing conversation when one already exists', async () => {
146
- // Mock existing conversation state
147
- const mockState = {
148
- conversationId: 'test-conversation-id',
149
- projectPath: '/test/project/path',
150
- gitBranch: 'main',
151
- currentPhase: 'idle',
152
- planFilePath: '/test/project/path/.vibe/development-plan.md',
153
- workflowName: 'existing-workflow',
154
- createdAt: '2025-06-25T00:00:00.000Z',
155
- updatedAt: '2025-06-25T00:00:00.000Z',
156
- };
157
-
158
- mockGetConversationState.mockResolvedValue(mockState);
159
-
160
- // Call the method with a different workflow
161
- const result = await conversationManager.createConversationContext(
162
- 'mock-different-workflow'
163
- );
164
-
165
- // Verify result is the existing conversation
166
- expect(result).toEqual({
167
- conversationId: 'test-conversation-id',
168
- projectPath: '/test/project/path',
169
- gitBranch: 'main',
170
- currentPhase: 'idle',
171
- planFilePath: '/test/project/path/.vibe/development-plan.md',
172
- workflowName: 'existing-workflow',
173
- });
174
-
175
- // Verify database was NOT called to save a new state
176
- expect(mockSaveConversationState).not.toHaveBeenCalled();
177
- });
178
- });
179
- });
@@ -1,174 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { WorkflowManager } from '@codemcp/workflows-core';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { tmpdir } from 'node:os';
6
-
7
- describe('Custom Workflow Loading', () => {
8
- let testProjectPath: string;
9
- let originalEnv: string | undefined;
10
-
11
- beforeEach(() => {
12
- originalEnv = process.env.VIBE_WORKFLOW_DOMAINS;
13
- testProjectPath = fs.mkdtempSync(
14
- path.join(tmpdir(), 'custom-workflow-test-')
15
- );
16
- });
17
-
18
- afterEach(() => {
19
- if (originalEnv !== undefined) {
20
- process.env.VIBE_WORKFLOW_DOMAINS = originalEnv;
21
- } else {
22
- delete process.env.VIBE_WORKFLOW_DOMAINS;
23
- }
24
-
25
- fs.rmSync(testProjectPath, { recursive: true, force: true });
26
- });
27
-
28
- it('should load custom workflow from .vibe/workflows directory', () => {
29
- process.env.VIBE_WORKFLOW_DOMAINS = 'code';
30
-
31
- const workflowsDir = path.join(testProjectPath, '.vibe', 'workflows');
32
- fs.mkdirSync(workflowsDir, { recursive: true });
33
-
34
- // Copy existing minor workflow and customize it
35
- const minorPath = path.join(
36
- __dirname,
37
- '..',
38
- '..',
39
- 'resources',
40
- 'workflows',
41
- 'minor.yaml'
42
- );
43
- const originalContent = fs.readFileSync(minorPath, 'utf8');
44
- const customContent = originalContent
45
- .replace("name: 'minor'", "name: 'my-custom-workflow'")
46
- .replace(
47
- /description: .*/,
48
- "description: 'My completely custom workflow'"
49
- );
50
-
51
- fs.writeFileSync(path.join(workflowsDir, 'custom.yaml'), customContent);
52
-
53
- const manager = new WorkflowManager();
54
-
55
- // Load project workflows
56
- const workflows = manager.getAvailableWorkflowsForProject(testProjectPath);
57
-
58
- // Should load custom workflow
59
- const customWf = manager.getWorkflow('my-custom-workflow');
60
- expect(customWf?.name).toBe('my-custom-workflow');
61
- expect(customWf?.description).toBe('My completely custom workflow');
62
- expect(customWf?.initial_state).toBe('explore');
63
- expect(Object.keys(customWf?.states || {})).toEqual([
64
- 'explore',
65
- 'implement',
66
- 'finalize',
67
- ]);
68
-
69
- // Should be in workflow list
70
- const customInList = workflows.find(w => w.name === 'my-custom-workflow');
71
- expect(customInList?.description).toBe('My completely custom workflow');
72
- expect(customInList?.phases).toEqual(['explore', 'implement', 'finalize']);
73
- });
74
-
75
- it('should load multiple custom workflows', () => {
76
- process.env.VIBE_WORKFLOW_DOMAINS = 'code';
77
-
78
- const workflowsDir = path.join(testProjectPath, '.vibe', 'workflows');
79
- fs.mkdirSync(workflowsDir, { recursive: true });
80
-
81
- // Copy and customize first workflow
82
- const minorPath = path.join(
83
- __dirname,
84
- '..',
85
- '..',
86
- 'resources',
87
- 'workflows',
88
- 'minor.yaml'
89
- );
90
- const minorContent = fs.readFileSync(minorPath, 'utf8');
91
- const workflow1 = minorContent
92
- .replace("name: 'minor'", "name: 'workflow-one'")
93
- .replace(/description: .*/, "description: 'First custom workflow'");
94
-
95
- // Copy and customize second workflow
96
- const bugfixPath = path.join(
97
- __dirname,
98
- '..',
99
- '..',
100
- 'resources',
101
- 'workflows',
102
- 'bugfix.yaml'
103
- );
104
- const bugfixContent = fs.readFileSync(bugfixPath, 'utf8');
105
- const workflow2 = bugfixContent
106
- .replace("name: 'bugfix'", "name: 'workflow-two'")
107
- .replace(/description: .*/, "description: 'Second custom workflow'");
108
-
109
- fs.writeFileSync(path.join(workflowsDir, 'first.yaml'), workflow1);
110
- fs.writeFileSync(path.join(workflowsDir, 'second.yaml'), workflow2);
111
-
112
- const manager = new WorkflowManager();
113
-
114
- // Load project workflows
115
- const workflows = manager.getAvailableWorkflowsForProject(testProjectPath);
116
-
117
- // Should load both custom workflows
118
- const wf1 = manager.getWorkflow('workflow-one');
119
- const wf2 = manager.getWorkflow('workflow-two');
120
-
121
- expect(wf1?.name).toBe('workflow-one');
122
- expect(wf1?.description).toBe('First custom workflow');
123
-
124
- expect(wf2?.name).toBe('workflow-two');
125
- expect(wf2?.description).toBe('Second custom workflow');
126
-
127
- // Both should be in workflow list
128
- expect(workflows.some(w => w.name === 'workflow-one')).toBe(true);
129
- expect(workflows.some(w => w.name === 'workflow-two')).toBe(true);
130
-
131
- // Should still have predefined workflows too
132
- expect(workflows.some(w => w.name === 'waterfall')).toBe(true);
133
- });
134
-
135
- it('should ignore domain filtering for custom workflows', () => {
136
- process.env.VIBE_WORKFLOW_DOMAINS = 'code'; // Only code domain
137
-
138
- const workflowsDir = path.join(testProjectPath, '.vibe', 'workflows');
139
- fs.mkdirSync(workflowsDir, { recursive: true });
140
-
141
- // Copy posts workflow (office domain) and customize it
142
- const postsPath = path.join(
143
- __dirname,
144
- '..',
145
- '..',
146
- 'resources',
147
- 'workflows',
148
- 'posts.yaml'
149
- );
150
- const postsContent = fs.readFileSync(postsPath, 'utf8');
151
- const officeWorkflow = postsContent
152
- .replace('name: posts', 'name: custom-office-workflow')
153
- .replace(/description: .*/, "description: 'Custom office workflow'");
154
-
155
- fs.writeFileSync(path.join(workflowsDir, 'office.yaml'), officeWorkflow);
156
-
157
- const manager = new WorkflowManager();
158
-
159
- // Load project workflows
160
- const workflows = manager.getAvailableWorkflowsForProject(testProjectPath);
161
-
162
- // Custom office workflow should be available despite domain filtering
163
- const customOffice = manager.getWorkflow('custom-office-workflow');
164
- expect(customOffice?.name).toBe('custom-office-workflow');
165
- expect(customOffice?.metadata?.domain).toBe('office');
166
-
167
- // Should be in workflow list
168
- expect(workflows.some(w => w.name === 'custom-office-workflow')).toBe(true);
169
-
170
- // Predefined office workflows should still be filtered out
171
- expect(workflows.some(w => w.name === 'posts')).toBe(false);
172
- expect(workflows.some(w => w.name === 'slides')).toBe(false);
173
- });
174
- });
@@ -1,338 +0,0 @@
1
- /**
2
- * Tests for directory linking and extension preservation functionality
3
- *
4
- * Tests the fixes for:
5
- * - Issue 1: Directory linking support
6
- * - Issue 2: Extension preservation in symlinks
7
- */
8
-
9
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
- import { PathValidationUtils } from '@codemcp/workflows-core';
11
- import { ProjectDocsManager } from '@codemcp/workflows-core';
12
- import { join } from 'node:path';
13
- import { tmpdir } from 'node:os';
14
- import { mkdir, writeFile, rm, readlink, lstat } from 'node:fs/promises';
15
-
16
- describe('Directory Linking and Extension Preservation', () => {
17
- let testProjectPath: string;
18
- let projectDocsManager: ProjectDocsManager;
19
-
20
- beforeEach(async () => {
21
- // Create test project directory
22
- testProjectPath = join(tmpdir(), `dir-ext-test-${Date.now()}`);
23
- await mkdir(testProjectPath, { recursive: true });
24
-
25
- projectDocsManager = new ProjectDocsManager();
26
- });
27
-
28
- afterEach(async () => {
29
- // Clean up test directory
30
- try {
31
- await rm(testProjectPath, { recursive: true });
32
- } catch {
33
- // Ignore cleanup errors
34
- }
35
- });
36
-
37
- describe('Directory Linking Support (Issue 1 Fix)', () => {
38
- it('should validate directories with validateFileOrDirectoryPath', async () => {
39
- // Create test directory
40
- const docsDir = join(testProjectPath, 'docs');
41
- await mkdir(docsDir, { recursive: true });
42
- await writeFile(join(docsDir, 'index.md'), '# Documentation');
43
-
44
- const result = await PathValidationUtils.validateFileOrDirectoryPath(
45
- docsDir,
46
- testProjectPath
47
- );
48
-
49
- expect(result.isValid).toBe(true);
50
- expect(result.resolvedPath).toBe(docsDir);
51
- });
52
-
53
- it('should reject directories with old validateFilePath method', async () => {
54
- // Create test directory
55
- const docsDir = join(testProjectPath, 'docs');
56
- await mkdir(docsDir, { recursive: true });
57
- await writeFile(join(docsDir, 'index.md'), '# Documentation');
58
-
59
- const result = await PathValidationUtils.validateFilePath(
60
- docsDir,
61
- testProjectPath
62
- );
63
-
64
- expect(result.isValid).toBe(false);
65
- expect(result.error).toContain('directory, not a file');
66
- });
67
-
68
- it('should recognize directories as valid file paths in validateParameter', async () => {
69
- // Create test directory
70
- const docsDir = join(testProjectPath, 'docs');
71
- await mkdir(docsDir, { recursive: true });
72
- await writeFile(join(docsDir, 'index.md'), '# Documentation');
73
-
74
- const result = await PathValidationUtils.validateParameter(
75
- docsDir,
76
- ['arc42'],
77
- testProjectPath
78
- );
79
-
80
- expect(result.isTemplate).toBe(false);
81
- expect(result.isFilePath).toBe(true);
82
- expect(result.resolvedPath).toBe(docsDir);
83
- });
84
-
85
- it('should create symlinks to directories', async () => {
86
- // Create test directory
87
- const docsDir = join(testProjectPath, 'docs');
88
- await mkdir(docsDir, { recursive: true });
89
- await writeFile(join(docsDir, 'architecture.md'), '# Architecture');
90
-
91
- const result = await projectDocsManager.createOrLinkProjectDocs(
92
- testProjectPath,
93
- {}, // No templates
94
- { architecture: docsDir }
95
- );
96
-
97
- expect(result.linked).toContain('architecture');
98
- expect(result.created.length).toBe(2); // requirements and design from templates
99
- expect(result.skipped.length).toBe(0);
100
-
101
- // Verify symlink was created
102
- const paths = await projectDocsManager.getDocumentPathsWithExtensions(
103
- testProjectPath,
104
- { architecture: docsDir }
105
- );
106
- const stats = await lstat(paths.architecture);
107
- expect(stats.isSymbolicLink()).toBe(true);
108
-
109
- const linkTarget = await readlink(paths.architecture);
110
- expect(linkTarget).toContain('docs');
111
- });
112
- });
113
-
114
- describe('Extension Preservation (Issue 2 Fix)', () => {
115
- it('should preserve file extensions in getDocumentPathsWithExtensions', async () => {
116
- // Create test files with different extensions
117
- await writeFile(join(testProjectPath, 'arch.adoc'), '= Architecture');
118
- await writeFile(join(testProjectPath, 'reqs.docx'), 'Requirements');
119
- await writeFile(join(testProjectPath, 'design.txt'), 'Design');
120
-
121
- const sourcePaths = {
122
- architecture: join(testProjectPath, 'arch.adoc'),
123
- requirements: join(testProjectPath, 'reqs.docx'),
124
- design: join(testProjectPath, 'design.txt'),
125
- };
126
-
127
- const paths = await projectDocsManager.getDocumentPathsWithExtensions(
128
- testProjectPath,
129
- sourcePaths
130
- );
131
-
132
- expect(paths.architecture.endsWith('.adoc')).toBe(true);
133
- expect(paths.requirements.endsWith('.docx')).toBe(true);
134
- expect(paths.design.endsWith('.txt')).toBe(true);
135
- });
136
-
137
- it('should use .md extension for templates (backward compatibility)', async () => {
138
- const paths =
139
- await projectDocsManager.getDocumentPathsWithExtensions(
140
- testProjectPath
141
- );
142
-
143
- expect(paths.architecture.endsWith('.md')).toBe(true);
144
- expect(paths.requirements.endsWith('.md')).toBe(true);
145
- expect(paths.design.endsWith('.md')).toBe(true);
146
- });
147
-
148
- it('should create symlinks with preserved extensions', async () => {
149
- // Create test files
150
- await writeFile(
151
- join(testProjectPath, 'architecture.adoc'),
152
- '= Architecture\n\nAsciiDoc format'
153
- );
154
- await writeFile(
155
- join(testProjectPath, 'requirements.docx'),
156
- 'Word document'
157
- );
158
- await writeFile(join(testProjectPath, 'design.txt'), 'Plain text');
159
-
160
- const sourcePaths = {
161
- architecture: join(testProjectPath, 'architecture.adoc'),
162
- requirements: join(testProjectPath, 'requirements.docx'),
163
- design: join(testProjectPath, 'design.txt'),
164
- };
165
-
166
- const result = await projectDocsManager.createOrLinkProjectDocs(
167
- testProjectPath,
168
- {}, // No templates
169
- sourcePaths
170
- );
171
-
172
- expect(result.linked).toEqual([
173
- 'architecture.adoc',
174
- 'requirements.docx',
175
- 'design.txt',
176
- ]);
177
- expect(result.created).toEqual([]);
178
- expect(result.skipped).toEqual([]);
179
-
180
- // Verify symlinks exist with correct extensions
181
- const paths = await projectDocsManager.getDocumentPathsWithExtensions(
182
- testProjectPath,
183
- sourcePaths
184
- );
185
-
186
- const archStats = await lstat(paths.architecture);
187
- expect(archStats.isSymbolicLink()).toBe(true);
188
-
189
- const reqStats = await lstat(paths.requirements);
190
- expect(reqStats.isSymbolicLink()).toBe(true);
191
-
192
- const designStats = await lstat(paths.design);
193
- expect(designStats.isSymbolicLink()).toBe(true);
194
- });
195
-
196
- it('should handle mixed templates and file paths with correct extensions', async () => {
197
- // Create one test file
198
- await writeFile(
199
- join(testProjectPath, 'existing-design.adoc'),
200
- '= Design\n\nExisting design doc'
201
- );
202
-
203
- const result = await projectDocsManager.createOrLinkProjectDocs(
204
- testProjectPath,
205
- {
206
- architecture: 'freestyle', // Template
207
- requirements: 'freestyle', // Template
208
- },
209
- {
210
- design: join(testProjectPath, 'existing-design.adoc'), // File path
211
- }
212
- );
213
-
214
- expect(result.created).toEqual(['architecture.md', 'requirements.md']);
215
- expect(result.linked).toEqual(['design.adoc']);
216
- expect(result.skipped).toEqual([]);
217
- });
218
-
219
- it('should handle files without extensions', async () => {
220
- // Create file without extension
221
- await writeFile(
222
- join(testProjectPath, 'README'),
223
- '# Project\n\nNo extension file'
224
- );
225
-
226
- const sourcePaths = {
227
- architecture: join(testProjectPath, 'README'),
228
- };
229
-
230
- const paths = await projectDocsManager.getDocumentPathsWithExtensions(
231
- testProjectPath,
232
- sourcePaths
233
- );
234
-
235
- // Should default to .md for files without extensions
236
- expect(paths.architecture.endsWith('.md')).toBe(true);
237
- });
238
-
239
- it('should handle directories with no extension', async () => {
240
- // Create test directory
241
- const docsDir = join(testProjectPath, 'documentation');
242
- await mkdir(docsDir, { recursive: true });
243
- await writeFile(join(docsDir, 'index.md'), '# Documentation Index');
244
-
245
- const sourcePaths = {
246
- architecture: docsDir,
247
- };
248
-
249
- const paths = await projectDocsManager.getDocumentPathsWithExtensions(
250
- testProjectPath,
251
- sourcePaths
252
- );
253
-
254
- // Should use standardized document type name
255
- expect(paths.architecture.endsWith('architecture')).toBe(true);
256
- expect(paths.architecture).not.toContain('.md');
257
- });
258
- });
259
-
260
- describe('Backward Compatibility', () => {
261
- it('should maintain old getDocumentPaths behavior', () => {
262
- const paths = projectDocsManager.getDocumentPaths(testProjectPath);
263
-
264
- expect(paths.architecture.endsWith('architecture.md')).toBe(true);
265
- expect(paths.requirements.endsWith('requirements.md')).toBe(true);
266
- expect(paths.design.endsWith('design.md')).toBe(true);
267
- });
268
-
269
- it('should maintain old getVariableSubstitutions behavior', () => {
270
- const substitutions =
271
- projectDocsManager.getVariableSubstitutions(testProjectPath);
272
-
273
- expect(
274
- substitutions['$ARCHITECTURE_DOC'].endsWith('architecture.md')
275
- ).toBe(true);
276
- expect(
277
- substitutions['$REQUIREMENTS_DOC'].endsWith('requirements.md')
278
- ).toBe(true);
279
- expect(substitutions['$DESIGN_DOC'].endsWith('design.md')).toBe(true);
280
- });
281
-
282
- it('should provide new getVariableSubstitutionsWithExtensions method', async () => {
283
- // Create test files
284
- await writeFile(join(testProjectPath, 'arch.adoc'), '= Architecture');
285
-
286
- const sourcePaths = {
287
- architecture: join(testProjectPath, 'arch.adoc'),
288
- };
289
-
290
- const substitutions =
291
- await projectDocsManager.getVariableSubstitutionsWithExtensions(
292
- testProjectPath,
293
- sourcePaths
294
- );
295
-
296
- expect(
297
- substitutions['$ARCHITECTURE_DOC'].endsWith('architecture.adoc')
298
- ).toBe(true);
299
- expect(
300
- substitutions['$REQUIREMENTS_DOC'].endsWith('requirements.md')
301
- ).toBe(true); // Default for no source
302
- expect(substitutions['$DESIGN_DOC'].endsWith('design.md')).toBe(true); // Default for no source
303
- });
304
- });
305
-
306
- describe('Error Handling', () => {
307
- it('should handle non-existent source paths gracefully', async () => {
308
- const sourcePaths = {
309
- architecture: join(testProjectPath, 'nonexistent.adoc'),
310
- };
311
-
312
- const paths = await projectDocsManager.getDocumentPathsWithExtensions(
313
- testProjectPath,
314
- sourcePaths
315
- );
316
-
317
- // Should default to .md when source doesn't exist
318
- expect(paths.architecture.endsWith('.md')).toBe(true);
319
- });
320
-
321
- it('should validate security boundaries for directories', async () => {
322
- // Test with an absolute path that's clearly outside any reasonable project boundary
323
- // This tests the security logic without relying on filesystem creation
324
- const maliciousPath = '/etc/passwd';
325
-
326
- const result = await PathValidationUtils.validateFileOrDirectoryPath(
327
- maliciousPath,
328
- testProjectPath
329
- );
330
-
331
- // This should fail because /etc/passwd doesn't exist as a directory we can create,
332
- // but more importantly, it tests that we're not allowing arbitrary system paths
333
- expect(result.isValid).toBe(false);
334
- // The error could be either "outside project boundaries" or "not found" - both are acceptable
335
- expect(result.error).toBeDefined();
336
- });
337
- });
338
- });