@projitive/mcp 2.0.4 → 2.1.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/output/package.json +1 -1
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +20 -9
- package/output/source/common/index.js +1 -0
- package/output/source/common/linter.js +3 -1
- package/output/source/common/response.js +51 -67
- package/output/source/common/tool.js +43 -0
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +17 -1
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.test.js +18 -0
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +210 -257
- package/output/source/tools/project.test.js +136 -4
- package/output/source/tools/roadmap.js +182 -216
- package/output/source/tools/roadmap.test.js +187 -0
- package/output/source/tools/task.js +598 -508
- package/output/source/tools/task.test.js +323 -2
- package/output/source/types.js +6 -0
- package/package.json +1 -1
|
@@ -2,7 +2,21 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
+
import { replaceRoadmapsInStore } from '../common/store.js';
|
|
5
6
|
import { isValidRoadmapId, collectRoadmapLintSuggestions, loadRoadmapDocument, renderRoadmapMarkdown, registerRoadmapTools } from './roadmap.js';
|
|
7
|
+
async function createGovernanceWorkspace() {
|
|
8
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-roadmap-workspace-'));
|
|
9
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
10
|
+
const dbPath = path.join(governanceDir, '.projitive');
|
|
11
|
+
await fs.mkdir(governanceDir, { recursive: true });
|
|
12
|
+
await fs.writeFile(dbPath, '', 'utf-8');
|
|
13
|
+
return { projectRoot, governanceDir, dbPath };
|
|
14
|
+
}
|
|
15
|
+
function getRoadmapToolHandler(mockServer, toolName) {
|
|
16
|
+
const call = mockServer.registerTool.mock.calls.find((entry) => entry[0] === toolName);
|
|
17
|
+
expect(call).toBeTruthy();
|
|
18
|
+
return call?.[2];
|
|
19
|
+
}
|
|
6
20
|
describe('roadmap module', () => {
|
|
7
21
|
let tempDir;
|
|
8
22
|
beforeAll(async () => {
|
|
@@ -47,6 +61,20 @@ describe('roadmap module', () => {
|
|
|
47
61
|
const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], tasks);
|
|
48
62
|
expect(suggestions.some(s => s.includes('TASK_REFS_EMPTY'))).toBe(true);
|
|
49
63
|
});
|
|
64
|
+
it('collects lint suggestion for tasks with unknown roadmap refs', () => {
|
|
65
|
+
const tasks = [{
|
|
66
|
+
id: 'TASK-0001',
|
|
67
|
+
title: 'Orphaned Task',
|
|
68
|
+
status: 'TODO',
|
|
69
|
+
owner: 'ai-copilot',
|
|
70
|
+
summary: '',
|
|
71
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
72
|
+
links: [],
|
|
73
|
+
roadmapRefs: ['ROADMAP-9999'],
|
|
74
|
+
}];
|
|
75
|
+
const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], tasks);
|
|
76
|
+
expect(suggestions.some(s => s.includes('UNKNOWN_REFS'))).toBe(true);
|
|
77
|
+
});
|
|
50
78
|
it('loads from governance store and rewrites roadmap markdown view', async () => {
|
|
51
79
|
const governanceDir = path.join(tempDir, '.projitive-db');
|
|
52
80
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
@@ -79,5 +107,164 @@ describe('roadmap module', () => {
|
|
|
79
107
|
registerRoadmapTools(mockServer);
|
|
80
108
|
expect(spy.mock.calls.some((call) => call[0] === 'roadmapCreate')).toBe(true);
|
|
81
109
|
});
|
|
110
|
+
it('sorts milestones with same timestamp by ID descending', () => {
|
|
111
|
+
const markdown = renderRoadmapMarkdown([
|
|
112
|
+
{ id: 'ROADMAP-0001', title: 'First', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
113
|
+
{ id: 'ROADMAP-0003', title: 'Third', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
114
|
+
{ id: 'ROADMAP-0002', title: 'Second', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
115
|
+
]);
|
|
116
|
+
expect(markdown.indexOf('ROADMAP-0003')).toBeLessThan(markdown.indexOf('ROADMAP-0002'));
|
|
117
|
+
expect(markdown.indexOf('ROADMAP-0002')).toBeLessThan(markdown.indexOf('ROADMAP-0001'));
|
|
118
|
+
});
|
|
119
|
+
it('skips view write on second load with no changes', async () => {
|
|
120
|
+
const { projectRoot, governanceDir } = await createGovernanceWorkspace();
|
|
121
|
+
await loadRoadmapDocument(governanceDir);
|
|
122
|
+
const doc = await loadRoadmapDocument(governanceDir);
|
|
123
|
+
expect(doc.milestones).toHaveLength(0);
|
|
124
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('roadmapList tool handler', () => {
|
|
128
|
+
it('lists roadmap IDs and linked task count', async () => {
|
|
129
|
+
const { projectRoot, dbPath } = await createGovernanceWorkspace();
|
|
130
|
+
await replaceRoadmapsInStore(dbPath, [
|
|
131
|
+
{ id: 'ROADMAP-0001', title: 'First', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
132
|
+
{ id: 'ROADMAP-0002', title: 'Second', status: 'done', updatedAt: '2026-02-01T00:00:00.000Z' },
|
|
133
|
+
]);
|
|
134
|
+
const mockServer = { registerTool: vi.fn() };
|
|
135
|
+
registerRoadmapTools(mockServer);
|
|
136
|
+
const roadmapList = getRoadmapToolHandler(mockServer, 'roadmapList');
|
|
137
|
+
const result = await roadmapList({ projectPath: projectRoot });
|
|
138
|
+
expect(result.isError).toBeUndefined();
|
|
139
|
+
expect(result.content[0].text).toContain('roadmapCount: 2');
|
|
140
|
+
expect(result.content[0].text).toContain('ROADMAP-0001');
|
|
141
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
it('returns no nextCall when roadmap store is empty', async () => {
|
|
144
|
+
const { projectRoot } = await createGovernanceWorkspace();
|
|
145
|
+
const mockServer = { registerTool: vi.fn() };
|
|
146
|
+
registerRoadmapTools(mockServer);
|
|
147
|
+
const roadmapList = getRoadmapToolHandler(mockServer, 'roadmapList');
|
|
148
|
+
const result = await roadmapList({ projectPath: projectRoot });
|
|
149
|
+
expect(result.isError).toBeUndefined();
|
|
150
|
+
expect(result.content[0].text).toContain('roadmapCount: 0');
|
|
151
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('roadmapContext tool handler', () => {
|
|
155
|
+
it('returns error for invalid roadmap ID format', async () => {
|
|
156
|
+
const { projectRoot } = await createGovernanceWorkspace();
|
|
157
|
+
const mockServer = { registerTool: vi.fn() };
|
|
158
|
+
registerRoadmapTools(mockServer);
|
|
159
|
+
const roadmapContext = getRoadmapToolHandler(mockServer, 'roadmapContext');
|
|
160
|
+
const result = await roadmapContext({ projectPath: projectRoot, roadmapId: 'INVALID-ID' });
|
|
161
|
+
expect(result.isError).toBe(true);
|
|
162
|
+
expect(result.content[0].text).toContain('Invalid roadmap ID format');
|
|
163
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
164
|
+
});
|
|
165
|
+
it('returns context with related tasks and reference locations', async () => {
|
|
166
|
+
const { projectRoot, dbPath } = await createGovernanceWorkspace();
|
|
167
|
+
await replaceRoadmapsInStore(dbPath, [
|
|
168
|
+
{ id: 'ROADMAP-0001', title: 'Bootstrap', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
169
|
+
]);
|
|
170
|
+
const mockServer = { registerTool: vi.fn() };
|
|
171
|
+
registerRoadmapTools(mockServer);
|
|
172
|
+
const roadmapContext = getRoadmapToolHandler(mockServer, 'roadmapContext');
|
|
173
|
+
const result = await roadmapContext({ projectPath: projectRoot, roadmapId: 'ROADMAP-0001' });
|
|
174
|
+
expect(result.isError).toBeUndefined();
|
|
175
|
+
expect(result.content[0].text).toContain('ROADMAP-0001');
|
|
176
|
+
expect(result.content[0].text).toContain('relatedTasks: 0');
|
|
177
|
+
expect(result.content[0].text).toContain('roadmapView:');
|
|
178
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('roadmapCreate tool handler', () => {
|
|
182
|
+
it('returns error for invalid roadmap ID format', async () => {
|
|
183
|
+
const { projectRoot } = await createGovernanceWorkspace();
|
|
184
|
+
const mockServer = { registerTool: vi.fn() };
|
|
185
|
+
registerRoadmapTools(mockServer);
|
|
186
|
+
const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
|
|
187
|
+
const result = await roadmapCreate({ projectPath: projectRoot, roadmapId: 'BAD-FORMAT', title: 'Test' });
|
|
188
|
+
expect(result.isError).toBe(true);
|
|
189
|
+
expect(result.content[0].text).toContain('Invalid roadmap ID format');
|
|
190
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
191
|
+
});
|
|
192
|
+
it('returns error when roadmap ID already exists', async () => {
|
|
193
|
+
const { projectRoot, dbPath } = await createGovernanceWorkspace();
|
|
194
|
+
await replaceRoadmapsInStore(dbPath, [
|
|
195
|
+
{ id: 'ROADMAP-0001', title: 'Existing', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
196
|
+
]);
|
|
197
|
+
const mockServer = { registerTool: vi.fn() };
|
|
198
|
+
registerRoadmapTools(mockServer);
|
|
199
|
+
const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
|
|
200
|
+
const result = await roadmapCreate({ projectPath: projectRoot, roadmapId: 'ROADMAP-0001', title: 'Duplicate' });
|
|
201
|
+
expect(result.isError).toBe(true);
|
|
202
|
+
expect(result.content[0].text).toContain('already exists');
|
|
203
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
204
|
+
});
|
|
205
|
+
it('auto-generates next roadmap ID', async () => {
|
|
206
|
+
const { projectRoot, dbPath } = await createGovernanceWorkspace();
|
|
207
|
+
await replaceRoadmapsInStore(dbPath, [
|
|
208
|
+
{ id: 'ROADMAP-0005', title: 'Existing', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
209
|
+
]);
|
|
210
|
+
const mockServer = { registerTool: vi.fn() };
|
|
211
|
+
registerRoadmapTools(mockServer);
|
|
212
|
+
const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
|
|
213
|
+
const result = await roadmapCreate({ projectPath: projectRoot, title: 'New Milestone' });
|
|
214
|
+
expect(result.isError).toBeUndefined();
|
|
215
|
+
expect(result.content[0].text).toContain('ROADMAP-0006');
|
|
216
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
217
|
+
});
|
|
218
|
+
it('creates milestone with explicit ID, status done, and time', async () => {
|
|
219
|
+
const { projectRoot } = await createGovernanceWorkspace();
|
|
220
|
+
const mockServer = { registerTool: vi.fn() };
|
|
221
|
+
registerRoadmapTools(mockServer);
|
|
222
|
+
const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
|
|
223
|
+
const result = await roadmapCreate({ projectPath: projectRoot, roadmapId: 'ROADMAP-0001', title: 'Planned', status: 'done', time: '2026-Q2' });
|
|
224
|
+
expect(result.isError).toBeUndefined();
|
|
225
|
+
expect(result.content[0].text).toContain('ROADMAP-0001');
|
|
226
|
+
expect(result.content[0].text).toContain('done');
|
|
227
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe('roadmapUpdate tool handler', () => {
|
|
231
|
+
it('returns error for invalid roadmap ID format', async () => {
|
|
232
|
+
const { projectRoot } = await createGovernanceWorkspace();
|
|
233
|
+
const mockServer = { registerTool: vi.fn() };
|
|
234
|
+
registerRoadmapTools(mockServer);
|
|
235
|
+
const roadmapUpdate = getRoadmapToolHandler(mockServer, 'roadmapUpdate');
|
|
236
|
+
const result = await roadmapUpdate({ projectPath: projectRoot, roadmapId: 'NOT-VALID', updates: { title: 'New' } });
|
|
237
|
+
expect(result.isError).toBe(true);
|
|
238
|
+
expect(result.content[0].text).toContain('Invalid roadmap ID format');
|
|
239
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
240
|
+
});
|
|
241
|
+
it('returns error when roadmap milestone not found', async () => {
|
|
242
|
+
const { projectRoot } = await createGovernanceWorkspace();
|
|
243
|
+
const mockServer = { registerTool: vi.fn() };
|
|
244
|
+
registerRoadmapTools(mockServer);
|
|
245
|
+
const roadmapUpdate = getRoadmapToolHandler(mockServer, 'roadmapUpdate');
|
|
246
|
+
const result = await roadmapUpdate({ projectPath: projectRoot, roadmapId: 'ROADMAP-0099', updates: { title: 'Does not exist' } });
|
|
247
|
+
expect(result.isError).toBe(true);
|
|
248
|
+
expect(result.content[0].text).toContain('not found');
|
|
249
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
250
|
+
});
|
|
251
|
+
it('updates milestone title, status, and time', async () => {
|
|
252
|
+
const { projectRoot, dbPath } = await createGovernanceWorkspace();
|
|
253
|
+
await replaceRoadmapsInStore(dbPath, [
|
|
254
|
+
{ id: 'ROADMAP-0001', title: 'Original', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
|
|
255
|
+
]);
|
|
256
|
+
const mockServer = { registerTool: vi.fn() };
|
|
257
|
+
registerRoadmapTools(mockServer);
|
|
258
|
+
const roadmapUpdate = getRoadmapToolHandler(mockServer, 'roadmapUpdate');
|
|
259
|
+
const result = await roadmapUpdate({
|
|
260
|
+
projectPath: projectRoot,
|
|
261
|
+
roadmapId: 'ROADMAP-0001',
|
|
262
|
+
updates: { title: 'Updated', status: 'done', time: '2026-Q3' },
|
|
263
|
+
});
|
|
264
|
+
expect(result.isError).toBeUndefined();
|
|
265
|
+
expect(result.content[0].text).toContain('ROADMAP-0001');
|
|
266
|
+
expect(result.content[0].text).toContain('done');
|
|
267
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
268
|
+
});
|
|
82
269
|
});
|
|
83
270
|
});
|