@codemcp/workflows-core 3.1.22 → 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.
- package/package.json +8 -3
- package/resources/templates/architecture/arc42/arc42-template-EN.md +1077 -0
- package/resources/templates/architecture/arc42/images/01_2_iso-25010-topics-EN.drawio-2023.png +0 -0
- package/resources/templates/architecture/arc42/images/01_2_iso-25010-topics-EN.drawio.png +0 -0
- package/resources/templates/architecture/arc42/images/05_building_blocks-EN.png +0 -0
- package/resources/templates/architecture/arc42/images/08-concepts-EN.drawio.png +0 -0
- package/resources/templates/architecture/arc42/images/arc42-logo.png +0 -0
- package/resources/templates/architecture/c4.md +224 -0
- package/resources/templates/architecture/freestyle.md +53 -0
- package/resources/templates/architecture/none.md +17 -0
- package/resources/templates/design/comprehensive.md +207 -0
- package/resources/templates/design/freestyle.md +37 -0
- package/resources/templates/design/none.md +17 -0
- package/resources/templates/requirements/ears.md +90 -0
- package/resources/templates/requirements/freestyle.md +42 -0
- package/resources/templates/requirements/none.md +17 -0
- package/resources/workflows/big-bang-conversion.yaml +539 -0
- package/resources/workflows/boundary-testing.yaml +334 -0
- package/resources/workflows/bugfix.yaml +185 -0
- package/resources/workflows/business-analysis.yaml +671 -0
- package/resources/workflows/c4-analysis.yaml +485 -0
- package/resources/workflows/epcc.yaml +161 -0
- package/resources/workflows/greenfield.yaml +189 -0
- package/resources/workflows/minor.yaml +127 -0
- package/resources/workflows/posts.yaml +207 -0
- package/resources/workflows/slides.yaml +256 -0
- package/resources/workflows/tdd.yaml +157 -0
- package/resources/workflows/waterfall.yaml +195 -0
- package/.turbo/turbo-build.log +0 -4
- package/src/config-manager.ts +0 -96
- package/src/conversation-manager.ts +0 -489
- package/src/database.ts +0 -427
- package/src/file-detection-manager.ts +0 -302
- package/src/git-manager.ts +0 -64
- package/src/index.ts +0 -28
- package/src/instruction-generator.ts +0 -210
- package/src/interaction-logger.ts +0 -109
- package/src/logger.ts +0 -353
- package/src/path-validation-utils.ts +0 -261
- package/src/plan-manager.ts +0 -323
- package/src/project-docs-manager.ts +0 -523
- package/src/state-machine-loader.ts +0 -365
- package/src/state-machine-types.ts +0 -72
- package/src/state-machine.ts +0 -370
- package/src/system-prompt-generator.ts +0 -122
- package/src/template-manager.ts +0 -328
- package/src/transition-engine.ts +0 -386
- package/src/types.ts +0 -60
- package/src/workflow-manager.ts +0 -606
- package/test/unit/conversation-manager.test.ts +0 -179
- package/test/unit/custom-workflow-loading.test.ts +0 -174
- package/test/unit/directory-linking-and-extensions.test.ts +0 -338
- package/test/unit/file-linking-integration.test.ts +0 -256
- package/test/unit/git-commit-integration.test.ts +0 -91
- package/test/unit/git-manager.test.ts +0 -86
- package/test/unit/install-workflow.test.ts +0 -138
- package/test/unit/instruction-generator.test.ts +0 -247
- package/test/unit/list-workflows-filtering.test.ts +0 -68
- package/test/unit/none-template-functionality.test.ts +0 -224
- package/test/unit/project-docs-manager.test.ts +0 -337
- package/test/unit/state-machine-loader.test.ts +0 -234
- package/test/unit/template-manager.test.ts +0 -217
- package/test/unit/validate-workflow-name.test.ts +0 -150
- package/test/unit/workflow-domain-filtering.test.ts +0 -75
- package/test/unit/workflow-enum-generation.test.ts +0 -92
- package/test/unit/workflow-manager-enhanced-path-resolution.test.ts +0 -369
- package/test/unit/workflow-manager-path-resolution.test.ts +0 -150
- package/test/unit/workflow-migration.test.ts +0 -155
- package/test/unit/workflow-override-by-name.test.ts +0 -116
- package/test/unit/workflow-prioritization.test.ts +0 -38
- package/test/unit/workflow-validation.test.ts +0 -303
- package/test/utils/e2e-test-setup.ts +0 -453
- package/test/utils/run-server-in-dir.sh +0 -27
- package/test/utils/temp-files.ts +0 -308
- package/test/utils/test-access.ts +0 -79
- package/test/utils/test-helpers.ts +0 -286
- package/test/utils/test-setup.ts +0 -78
- package/tsconfig.build.json +0 -21
- package/tsconfig.json +0 -8
- 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
|
-
});
|