@smallironman/mcp-memory-keeper 0.12.2-fork1
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/CHANGELOG.md +542 -0
- package/LICENSE +21 -0
- package/README.md +1281 -0
- package/bin/mcp-memory-keeper +54 -0
- package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
- package/dist/__tests__/e2e/server-e2e.test.js +341 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +1054 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +411 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
- package/dist/__tests__/integration/git-integration.test.js +241 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +291 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +305 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4425 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +2017 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +780 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/token-limits.js +350 -0
- package/dist/utils/tool-profiles.js +242 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +85 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const token_limits_1 = require("../../utils/token-limits");
|
|
4
|
+
describe('Token Limit Utilities', () => {
|
|
5
|
+
describe('estimateTokens', () => {
|
|
6
|
+
it('should estimate tokens more conservatively than before', () => {
|
|
7
|
+
const text = 'This is a test string';
|
|
8
|
+
const tokens = (0, token_limits_1.estimateTokens)(text);
|
|
9
|
+
// Using 3.5 chars per token instead of 4
|
|
10
|
+
expect(tokens).toBe(Math.ceil(text.length / 3.5));
|
|
11
|
+
});
|
|
12
|
+
it('should handle empty strings', () => {
|
|
13
|
+
expect((0, token_limits_1.estimateTokens)('')).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
it('should handle large texts', () => {
|
|
16
|
+
const largeText = 'x'.repeat(10000);
|
|
17
|
+
const tokens = (0, token_limits_1.estimateTokens)(largeText);
|
|
18
|
+
expect(tokens).toBe(Math.ceil(10000 / 3.5));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('calculateSafeItemLimit', () => {
|
|
22
|
+
const createTestItems = (count, size) => {
|
|
23
|
+
const content = {
|
|
24
|
+
small: 'Small content',
|
|
25
|
+
medium: 'Medium content that is longer and has more details'.repeat(5),
|
|
26
|
+
large: 'Large content with substantial information'.repeat(20),
|
|
27
|
+
};
|
|
28
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
29
|
+
key: `item_${i}`,
|
|
30
|
+
value: content[size],
|
|
31
|
+
category: 'test',
|
|
32
|
+
priority: 'normal',
|
|
33
|
+
channel: 'test',
|
|
34
|
+
created_at: '2024-01-20T10:00:00Z',
|
|
35
|
+
updated_at: '2024-01-20T10:00:00Z',
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
it('should calculate safe limit for small items', () => {
|
|
39
|
+
const items = createTestItems(100, 'small');
|
|
40
|
+
const limit = (0, token_limits_1.calculateSafeItemLimit)(items, false);
|
|
41
|
+
expect(limit).toBeGreaterThan(0);
|
|
42
|
+
expect(limit).toBeLessThanOrEqual(100);
|
|
43
|
+
});
|
|
44
|
+
it('should calculate smaller limit for large items', () => {
|
|
45
|
+
const smallItems = createTestItems(100, 'small');
|
|
46
|
+
const largeItems = createTestItems(100, 'large');
|
|
47
|
+
const smallLimit = (0, token_limits_1.calculateSafeItemLimit)(smallItems, false);
|
|
48
|
+
const largeLimit = (0, token_limits_1.calculateSafeItemLimit)(largeItems, false);
|
|
49
|
+
expect(largeLimit).toBeLessThan(smallLimit);
|
|
50
|
+
});
|
|
51
|
+
it('should calculate smaller limit with metadata', () => {
|
|
52
|
+
// Create items with actual metadata that would be added
|
|
53
|
+
const items = createTestItems(100, 'medium').map(item => ({
|
|
54
|
+
...item,
|
|
55
|
+
metadata: JSON.stringify({
|
|
56
|
+
tags: ['test', 'example'],
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
additionalInfo: 'Extra metadata that adds size',
|
|
59
|
+
}),
|
|
60
|
+
size: 500,
|
|
61
|
+
}));
|
|
62
|
+
const withoutMetadataLimit = (0, token_limits_1.calculateSafeItemLimit)(items, false);
|
|
63
|
+
const withMetadataLimit = (0, token_limits_1.calculateSafeItemLimit)(items, true);
|
|
64
|
+
// When metadata is included, each item becomes larger due to additional fields
|
|
65
|
+
// This should result in fewer items fitting in the token limit
|
|
66
|
+
// If they're equal, it might mean both hit the maxItems constraint
|
|
67
|
+
expect(withMetadataLimit).toBeLessThanOrEqual(withoutMetadataLimit);
|
|
68
|
+
});
|
|
69
|
+
it('should respect min and max constraints', () => {
|
|
70
|
+
const items = createTestItems(1000, 'small');
|
|
71
|
+
const config = {
|
|
72
|
+
...token_limits_1.DEFAULT_TOKEN_CONFIG,
|
|
73
|
+
minItems: 5,
|
|
74
|
+
maxItems: 50,
|
|
75
|
+
};
|
|
76
|
+
const limit = (0, token_limits_1.calculateSafeItemLimit)(items, false, config);
|
|
77
|
+
expect(limit).toBeGreaterThanOrEqual(5);
|
|
78
|
+
expect(limit).toBeLessThanOrEqual(50);
|
|
79
|
+
});
|
|
80
|
+
it('should return 0 for empty array', () => {
|
|
81
|
+
const limit = (0, token_limits_1.calculateSafeItemLimit)([], false);
|
|
82
|
+
expect(limit).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('checkTokenLimit', () => {
|
|
86
|
+
const createLargeDataset = () => {
|
|
87
|
+
return Array.from({ length: 200 }, (_, i) => ({
|
|
88
|
+
key: `item_${i}`,
|
|
89
|
+
value: `Large content that would cause token overflow when many items are combined. `.repeat(10),
|
|
90
|
+
category: 'test',
|
|
91
|
+
priority: 'high',
|
|
92
|
+
metadata: JSON.stringify({ index: i }),
|
|
93
|
+
created_at: '2024-01-20T10:00:00Z',
|
|
94
|
+
updated_at: '2024-01-20T10:00:00Z',
|
|
95
|
+
}));
|
|
96
|
+
};
|
|
97
|
+
it('should detect when token limit is exceeded', () => {
|
|
98
|
+
const items = createLargeDataset();
|
|
99
|
+
const { exceedsLimit, estimatedTokens, safeItemCount } = (0, token_limits_1.checkTokenLimit)(items, true);
|
|
100
|
+
expect(exceedsLimit).toBe(true);
|
|
101
|
+
expect(estimatedTokens).toBeGreaterThan(token_limits_1.DEFAULT_TOKEN_CONFIG.mcpMaxTokens * token_limits_1.DEFAULT_TOKEN_CONFIG.safetyBuffer);
|
|
102
|
+
expect(safeItemCount).toBeLessThan(items.length);
|
|
103
|
+
});
|
|
104
|
+
it('should not exceed limit for small datasets', () => {
|
|
105
|
+
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
106
|
+
key: `item_${i}`,
|
|
107
|
+
value: 'Small content',
|
|
108
|
+
category: 'test',
|
|
109
|
+
}));
|
|
110
|
+
const { exceedsLimit, estimatedTokens } = (0, token_limits_1.checkTokenLimit)(items, false);
|
|
111
|
+
expect(exceedsLimit).toBe(false);
|
|
112
|
+
expect(estimatedTokens).toBeLessThan(token_limits_1.DEFAULT_TOKEN_CONFIG.mcpMaxTokens * token_limits_1.DEFAULT_TOKEN_CONFIG.safetyBuffer);
|
|
113
|
+
});
|
|
114
|
+
it('should handle metadata transformation', () => {
|
|
115
|
+
const items = [
|
|
116
|
+
{
|
|
117
|
+
key: 'test',
|
|
118
|
+
value: 'content',
|
|
119
|
+
metadata: '{"tag": "test"}', // String metadata
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
const { exceedsLimit } = (0, token_limits_1.checkTokenLimit)(items, true);
|
|
123
|
+
expect(exceedsLimit).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('should handle invalid JSON in metadata gracefully', () => {
|
|
126
|
+
const items = [
|
|
127
|
+
{
|
|
128
|
+
key: 'test1',
|
|
129
|
+
value: 'content',
|
|
130
|
+
metadata: '{"valid": "json"}',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'test2',
|
|
134
|
+
value: 'content',
|
|
135
|
+
metadata: 'invalid json{', // Invalid JSON
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'test3',
|
|
139
|
+
value: 'content',
|
|
140
|
+
metadata: null,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
// Should not throw, but handle gracefully
|
|
144
|
+
expect(() => {
|
|
145
|
+
const { exceedsLimit, estimatedTokens } = (0, token_limits_1.checkTokenLimit)(items, true);
|
|
146
|
+
expect(exceedsLimit).toBe(false);
|
|
147
|
+
expect(estimatedTokens).toBeGreaterThan(0);
|
|
148
|
+
}).not.toThrow();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('estimateResponseOverhead', () => {
|
|
152
|
+
it('should estimate overhead for basic response', () => {
|
|
153
|
+
const overhead = (0, token_limits_1.estimateResponseOverhead)(10, false);
|
|
154
|
+
expect(overhead).toBeGreaterThan(0);
|
|
155
|
+
});
|
|
156
|
+
it('should estimate higher overhead with metadata', () => {
|
|
157
|
+
const basicOverhead = (0, token_limits_1.estimateResponseOverhead)(10, false);
|
|
158
|
+
const metadataOverhead = (0, token_limits_1.estimateResponseOverhead)(10, true);
|
|
159
|
+
expect(metadataOverhead).toBeGreaterThan(basicOverhead);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('calculateDynamicDefaultLimit', () => {
|
|
163
|
+
const createMockDb = (items) => ({
|
|
164
|
+
prepare: jest.fn(() => ({
|
|
165
|
+
all: jest.fn(() => items),
|
|
166
|
+
})),
|
|
167
|
+
});
|
|
168
|
+
it('should calculate limit based on session data', () => {
|
|
169
|
+
const sampleItems = Array.from({ length: 10 }, (_, i) => ({
|
|
170
|
+
key: `item_${i}`,
|
|
171
|
+
value: 'Sample content for calculation',
|
|
172
|
+
category: 'test',
|
|
173
|
+
}));
|
|
174
|
+
const mockDb = createMockDb(sampleItems);
|
|
175
|
+
const limit = (0, token_limits_1.calculateDynamicDefaultLimit)('test-session', false, mockDb);
|
|
176
|
+
expect(limit).toBeGreaterThan(0);
|
|
177
|
+
expect(limit % 10).toBe(0); // Should be rounded to nearest 10
|
|
178
|
+
});
|
|
179
|
+
it('should return conservative defaults for empty session', () => {
|
|
180
|
+
const mockDb = createMockDb([]);
|
|
181
|
+
const limitWithMetadata = (0, token_limits_1.calculateDynamicDefaultLimit)('test-session', true, mockDb);
|
|
182
|
+
const limitWithoutMetadata = (0, token_limits_1.calculateDynamicDefaultLimit)('test-session', false, mockDb);
|
|
183
|
+
expect(limitWithMetadata).toBe(30);
|
|
184
|
+
expect(limitWithoutMetadata).toBe(100);
|
|
185
|
+
});
|
|
186
|
+
it('should handle database errors gracefully', () => {
|
|
187
|
+
const mockDb = {
|
|
188
|
+
prepare: jest.fn(() => {
|
|
189
|
+
throw new Error('Database error');
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
const limit = (0, token_limits_1.calculateDynamicDefaultLimit)('test-session', true, mockDb);
|
|
193
|
+
expect(limit).toBe(30); // Fallback to conservative default
|
|
194
|
+
});
|
|
195
|
+
it('should return smaller limit with metadata', () => {
|
|
196
|
+
// Create items with substantial content that shows clear difference
|
|
197
|
+
const sampleItems = Array.from({ length: 10 }, (_, i) => ({
|
|
198
|
+
key: `item_${i}`,
|
|
199
|
+
value: 'Sample content that is moderately sized'.repeat(20), // Larger content
|
|
200
|
+
category: 'test',
|
|
201
|
+
priority: 'high',
|
|
202
|
+
channel: 'test-channel',
|
|
203
|
+
metadata: JSON.stringify({
|
|
204
|
+
index: i,
|
|
205
|
+
tags: ['tag1', 'tag2', 'tag3'],
|
|
206
|
+
description: 'Additional metadata that increases size',
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
}),
|
|
209
|
+
size: 1000,
|
|
210
|
+
created_at: '2024-01-20T10:00:00Z',
|
|
211
|
+
updated_at: '2024-01-20T10:00:00Z',
|
|
212
|
+
}));
|
|
213
|
+
const mockDb = createMockDb(sampleItems);
|
|
214
|
+
const withoutMetadata = (0, token_limits_1.calculateDynamicDefaultLimit)('test-session', false, mockDb);
|
|
215
|
+
const withMetadata = (0, token_limits_1.calculateDynamicDefaultLimit)('test-session', true, mockDb);
|
|
216
|
+
// Dynamic calculation should return smaller limit when metadata is included
|
|
217
|
+
// Both should be rounded to nearest 10
|
|
218
|
+
expect(withMetadata % 10).toBe(0);
|
|
219
|
+
expect(withoutMetadata % 10).toBe(0);
|
|
220
|
+
// With larger items and metadata, the difference should be clear
|
|
221
|
+
// If they're the same, both might be hitting minimum threshold
|
|
222
|
+
expect(withMetadata).toBeLessThanOrEqual(withoutMetadata);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const tool_profiles_1 = require("../../utils/tool-profiles");
|
|
40
|
+
describe('Tool Profiles', () => {
|
|
41
|
+
describe('ALL_TOOL_NAMES', () => {
|
|
42
|
+
it('should contain exactly 38 tool names', () => {
|
|
43
|
+
expect(tool_profiles_1.ALL_TOOL_NAMES).toHaveLength(38);
|
|
44
|
+
});
|
|
45
|
+
it('should have no duplicates', () => {
|
|
46
|
+
const unique = new Set(tool_profiles_1.ALL_TOOL_NAMES);
|
|
47
|
+
expect(unique.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
|
|
48
|
+
});
|
|
49
|
+
it('should all start with "context_"', () => {
|
|
50
|
+
for (const name of tool_profiles_1.ALL_TOOL_NAMES) {
|
|
51
|
+
expect(name).toMatch(/^context_/);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('ALL_TOOL_NAMES_SET', () => {
|
|
56
|
+
it('should be the same size as ALL_TOOL_NAMES', () => {
|
|
57
|
+
expect(tool_profiles_1.ALL_TOOL_NAMES_SET.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
|
|
58
|
+
});
|
|
59
|
+
it('should contain every entry from ALL_TOOL_NAMES', () => {
|
|
60
|
+
for (const name of tool_profiles_1.ALL_TOOL_NAMES) {
|
|
61
|
+
expect(tool_profiles_1.ALL_TOOL_NAMES_SET.has(name)).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('DEFAULT_PROFILES', () => {
|
|
66
|
+
it('should define minimal, standard, and full profiles', () => {
|
|
67
|
+
expect(tool_profiles_1.DEFAULT_PROFILES).toHaveProperty('minimal');
|
|
68
|
+
expect(tool_profiles_1.DEFAULT_PROFILES).toHaveProperty('standard');
|
|
69
|
+
expect(tool_profiles_1.DEFAULT_PROFILES).toHaveProperty('full');
|
|
70
|
+
});
|
|
71
|
+
it('minimal should have 8 tools', () => {
|
|
72
|
+
expect(tool_profiles_1.DEFAULT_PROFILES.minimal).toHaveLength(8);
|
|
73
|
+
});
|
|
74
|
+
it('standard should have 22 tools', () => {
|
|
75
|
+
expect(tool_profiles_1.DEFAULT_PROFILES.standard).toHaveLength(22);
|
|
76
|
+
});
|
|
77
|
+
it('full should have all 38 tools', () => {
|
|
78
|
+
expect(tool_profiles_1.DEFAULT_PROFILES.full).toHaveLength(38);
|
|
79
|
+
});
|
|
80
|
+
it('minimal should be a subset of standard', () => {
|
|
81
|
+
const standardSet = new Set(tool_profiles_1.DEFAULT_PROFILES.standard);
|
|
82
|
+
for (const tool of tool_profiles_1.DEFAULT_PROFILES.minimal) {
|
|
83
|
+
expect(standardSet.has(tool)).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it('standard should be a subset of full', () => {
|
|
87
|
+
const fullSet = new Set(tool_profiles_1.DEFAULT_PROFILES.full);
|
|
88
|
+
for (const tool of tool_profiles_1.DEFAULT_PROFILES.standard) {
|
|
89
|
+
expect(fullSet.has(tool)).toBe(true);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
it('all tools in each profile should be valid tool names', () => {
|
|
93
|
+
for (const [_profileName, tools] of Object.entries(tool_profiles_1.DEFAULT_PROFILES)) {
|
|
94
|
+
for (const tool of tools) {
|
|
95
|
+
expect(tool_profiles_1.ALL_TOOL_NAMES_SET.has(tool)).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it('full profile should match ALL_TOOL_NAMES exactly', () => {
|
|
100
|
+
expect(new Set(tool_profiles_1.DEFAULT_PROFILES.full)).toEqual(new Set(tool_profiles_1.ALL_TOOL_NAMES));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('validateToolNames', () => {
|
|
104
|
+
it('should return empty array for all valid names', () => {
|
|
105
|
+
expect((0, tool_profiles_1.validateToolNames)(['context_save', 'context_get'])).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
it('should return unknown names', () => {
|
|
108
|
+
const result = (0, tool_profiles_1.validateToolNames)(['context_save', 'nonexistent_tool', 'another_fake']);
|
|
109
|
+
expect(result).toEqual(['nonexistent_tool', 'another_fake']);
|
|
110
|
+
});
|
|
111
|
+
it('should handle empty array', () => {
|
|
112
|
+
expect((0, tool_profiles_1.validateToolNames)([])).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
it('should handle all-invalid array', () => {
|
|
115
|
+
const result = (0, tool_profiles_1.validateToolNames)(['fake1', 'fake2']);
|
|
116
|
+
expect(result).toEqual(['fake1', 'fake2']);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('loadConfigFile', () => {
|
|
120
|
+
const tmpDir = path.join(os.tmpdir(), 'mcp-mk-tool-profiles-test');
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
if (!fs.existsSync(tmpDir)) {
|
|
123
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
if (fs.existsSync(tmpDir)) {
|
|
128
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
it('should return null when config file does not exist', () => {
|
|
132
|
+
const result = (0, tool_profiles_1.loadConfigFile)(path.join(tmpDir, 'nonexistent.json'));
|
|
133
|
+
expect(result).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
it('should parse valid config file', () => {
|
|
136
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
137
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
138
|
+
profiles: {
|
|
139
|
+
minimal: ['context_save', 'context_get'],
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
143
|
+
expect(result).not.toBeNull();
|
|
144
|
+
expect(result.profiles.minimal).toEqual(['context_save', 'context_get']);
|
|
145
|
+
});
|
|
146
|
+
it('should return null and warn on JSON syntax error', () => {
|
|
147
|
+
const configPath = path.join(tmpDir, 'bad.json');
|
|
148
|
+
fs.writeFileSync(configPath, '{ invalid json }');
|
|
149
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
150
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
151
|
+
expect(result).toBeNull();
|
|
152
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to load config file'));
|
|
153
|
+
warnSpy.mockRestore();
|
|
154
|
+
});
|
|
155
|
+
it('should return null and warn on missing profiles key', () => {
|
|
156
|
+
const configPath = path.join(tmpDir, 'no-profiles.json');
|
|
157
|
+
fs.writeFileSync(configPath, JSON.stringify({ something: 'else' }));
|
|
158
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
159
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
160
|
+
expect(result).toBeNull();
|
|
161
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('missing a valid "profiles" key'));
|
|
162
|
+
warnSpy.mockRestore();
|
|
163
|
+
});
|
|
164
|
+
it('should handle config file with extra keys gracefully', () => {
|
|
165
|
+
const configPath = path.join(tmpDir, 'extra.json');
|
|
166
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
167
|
+
profiles: { custom: ['context_save'] },
|
|
168
|
+
extraKey: 'ignored',
|
|
169
|
+
}));
|
|
170
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
171
|
+
expect(result).not.toBeNull();
|
|
172
|
+
expect(result.profiles.custom).toEqual(['context_save']);
|
|
173
|
+
});
|
|
174
|
+
it('should return null when profiles is an array instead of object', () => {
|
|
175
|
+
const configPath = path.join(tmpDir, 'array-profiles.json');
|
|
176
|
+
fs.writeFileSync(configPath, JSON.stringify({ profiles: ['context_save'] }));
|
|
177
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
178
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
179
|
+
expect(result).toBeNull();
|
|
180
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('missing a valid "profiles" key'));
|
|
181
|
+
warnSpy.mockRestore();
|
|
182
|
+
});
|
|
183
|
+
it('should return null when profiles is null', () => {
|
|
184
|
+
const configPath = path.join(tmpDir, 'null-profiles.json');
|
|
185
|
+
fs.writeFileSync(configPath, JSON.stringify({ profiles: null }));
|
|
186
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
187
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
188
|
+
expect(result).toBeNull();
|
|
189
|
+
warnSpy.mockRestore();
|
|
190
|
+
});
|
|
191
|
+
it('should return null when root is an array', () => {
|
|
192
|
+
const configPath = path.join(tmpDir, 'root-array.json');
|
|
193
|
+
fs.writeFileSync(configPath, JSON.stringify([{ profiles: {} }]));
|
|
194
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
195
|
+
const result = (0, tool_profiles_1.loadConfigFile)(configPath);
|
|
196
|
+
expect(result).toBeNull();
|
|
197
|
+
warnSpy.mockRestore();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('resolveActiveProfile', () => {
|
|
201
|
+
const originalEnv = process.env.TOOL_PROFILE;
|
|
202
|
+
const tmpDir = path.join(os.tmpdir(), 'mcp-mk-resolve-profile-test');
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
delete process.env.TOOL_PROFILE;
|
|
205
|
+
if (!fs.existsSync(tmpDir)) {
|
|
206
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
afterEach(() => {
|
|
210
|
+
if (originalEnv !== undefined) {
|
|
211
|
+
process.env.TOOL_PROFILE = originalEnv;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
delete process.env.TOOL_PROFILE;
|
|
215
|
+
}
|
|
216
|
+
if (fs.existsSync(tmpDir)) {
|
|
217
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
it('should default to full when no env var and no config', () => {
|
|
221
|
+
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
222
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
223
|
+
expect(result.profileName).toBe('full');
|
|
224
|
+
expect(result.tools.size).toBe(38);
|
|
225
|
+
expect(result.source).toBe('default');
|
|
226
|
+
expect(result.warnings).toHaveLength(0);
|
|
227
|
+
});
|
|
228
|
+
it('should use env var profile from built-in defaults', () => {
|
|
229
|
+
process.env.TOOL_PROFILE = 'minimal';
|
|
230
|
+
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
231
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
232
|
+
expect(result.profileName).toBe('minimal');
|
|
233
|
+
expect(result.tools.size).toBe(8);
|
|
234
|
+
expect(result.source).toBe('env+builtin');
|
|
235
|
+
});
|
|
236
|
+
it('should use env var profile from config file (takes precedence)', () => {
|
|
237
|
+
process.env.TOOL_PROFILE = 'minimal';
|
|
238
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
239
|
+
// Config overrides minimal to have only 2 tools
|
|
240
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
241
|
+
profiles: { minimal: ['context_save', 'context_get'] },
|
|
242
|
+
}));
|
|
243
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
244
|
+
expect(result.profileName).toBe('minimal');
|
|
245
|
+
expect(result.tools.size).toBe(2);
|
|
246
|
+
expect(result.source).toBe('env+config');
|
|
247
|
+
});
|
|
248
|
+
it('should warn and fallback to full on invalid profile name', () => {
|
|
249
|
+
process.env.TOOL_PROFILE = 'nonexistent_profile';
|
|
250
|
+
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
251
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
252
|
+
expect(result.profileName).toBe('full');
|
|
253
|
+
expect(result.tools.size).toBe(38);
|
|
254
|
+
expect(result.source).toBe('default');
|
|
255
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
256
|
+
expect(result.warnings[0]).toContain('Unknown TOOL_PROFILE');
|
|
257
|
+
});
|
|
258
|
+
it('should warn on unknown tool names in profile', () => {
|
|
259
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
260
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
261
|
+
profiles: { custom: ['context_save', 'fake_tool'] },
|
|
262
|
+
}));
|
|
263
|
+
process.env.TOOL_PROFILE = 'custom';
|
|
264
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
265
|
+
expect(result.tools.size).toBe(1);
|
|
266
|
+
expect(result.tools.has('context_save')).toBe(true);
|
|
267
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
268
|
+
expect(result.warnings[0]).toContain('fake_tool');
|
|
269
|
+
});
|
|
270
|
+
it('should fallback to full when profile resolves to empty', () => {
|
|
271
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
272
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
273
|
+
profiles: { empty: ['fake_tool_1', 'fake_tool_2'] },
|
|
274
|
+
}));
|
|
275
|
+
process.env.TOOL_PROFILE = 'empty';
|
|
276
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
277
|
+
expect(result.profileName).toBe('full');
|
|
278
|
+
expect(result.tools.size).toBe(38);
|
|
279
|
+
expect(result.warnings.some(w => w.includes('no valid tools'))).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
it('should trim whitespace from TOOL_PROFILE env var', () => {
|
|
282
|
+
process.env.TOOL_PROFILE = ' minimal ';
|
|
283
|
+
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
284
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
285
|
+
expect(result.profileName).toBe('minimal');
|
|
286
|
+
expect(result.tools.size).toBe(8);
|
|
287
|
+
});
|
|
288
|
+
it('should support custom profiles from config file', () => {
|
|
289
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
290
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
291
|
+
profiles: {
|
|
292
|
+
my_workflow: ['context_save', 'context_get', 'context_diff'],
|
|
293
|
+
},
|
|
294
|
+
}));
|
|
295
|
+
process.env.TOOL_PROFILE = 'my_workflow';
|
|
296
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
297
|
+
expect(result.profileName).toBe('my_workflow');
|
|
298
|
+
expect(result.tools.size).toBe(3);
|
|
299
|
+
expect(result.source).toBe('env+config');
|
|
300
|
+
});
|
|
301
|
+
it('should use standard profile from env var', () => {
|
|
302
|
+
process.env.TOOL_PROFILE = 'standard';
|
|
303
|
+
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
304
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
305
|
+
expect(result.profileName).toBe('standard');
|
|
306
|
+
expect(result.tools.size).toBe(22);
|
|
307
|
+
expect(result.source).toBe('env+builtin');
|
|
308
|
+
});
|
|
309
|
+
it('should treat empty string TOOL_PROFILE as unset', () => {
|
|
310
|
+
process.env.TOOL_PROFILE = '';
|
|
311
|
+
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
312
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
313
|
+
expect(result.profileName).toBe('full');
|
|
314
|
+
expect(result.tools.size).toBe(38);
|
|
315
|
+
expect(result.source).toBe('default');
|
|
316
|
+
});
|
|
317
|
+
it('should handle config profile with null value gracefully', () => {
|
|
318
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
319
|
+
fs.writeFileSync(configPath, JSON.stringify({ profiles: { broken: null } }));
|
|
320
|
+
process.env.TOOL_PROFILE = 'broken';
|
|
321
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
322
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
323
|
+
// 'broken' not in built-ins, so falls through to unknown profile fallback
|
|
324
|
+
expect(result.profileName).toBe('full');
|
|
325
|
+
expect(result.tools.size).toBe(38);
|
|
326
|
+
expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
|
|
327
|
+
warnSpy.mockRestore();
|
|
328
|
+
});
|
|
329
|
+
it('should handle config profile with non-array value gracefully', () => {
|
|
330
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
331
|
+
fs.writeFileSync(configPath, JSON.stringify({ profiles: { broken: 'not-an-array' } }));
|
|
332
|
+
process.env.TOOL_PROFILE = 'broken';
|
|
333
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
334
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
335
|
+
expect(result.profileName).toBe('full');
|
|
336
|
+
expect(result.tools.size).toBe(38);
|
|
337
|
+
expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
|
|
338
|
+
warnSpy.mockRestore();
|
|
339
|
+
});
|
|
340
|
+
it('should handle config profile with non-string array elements gracefully', () => {
|
|
341
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
342
|
+
fs.writeFileSync(configPath, JSON.stringify({ profiles: { broken: [1, null, true] } }));
|
|
343
|
+
process.env.TOOL_PROFILE = 'broken';
|
|
344
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
345
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
346
|
+
expect(result.profileName).toBe('full');
|
|
347
|
+
expect(result.tools.size).toBe(38);
|
|
348
|
+
expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
|
|
349
|
+
warnSpy.mockRestore();
|
|
350
|
+
});
|
|
351
|
+
it('should fall back to built-in when config profile for known name is invalid', () => {
|
|
352
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
353
|
+
// Config has invalid 'minimal' entry, but built-in 'minimal' exists
|
|
354
|
+
fs.writeFileSync(configPath, JSON.stringify({ profiles: { minimal: 42 } }));
|
|
355
|
+
process.env.TOOL_PROFILE = 'minimal';
|
|
356
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
357
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
358
|
+
expect(result.profileName).toBe('minimal');
|
|
359
|
+
expect(result.tools.size).toBe(8);
|
|
360
|
+
expect(result.source).toBe('env+builtin');
|
|
361
|
+
expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
|
|
362
|
+
warnSpy.mockRestore();
|
|
363
|
+
});
|
|
364
|
+
it('should deduplicate tool names in profile via Set', () => {
|
|
365
|
+
const configPath = path.join(tmpDir, 'config.json');
|
|
366
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
367
|
+
profiles: { dupes: ['context_save', 'context_save', 'context_get'] },
|
|
368
|
+
}));
|
|
369
|
+
process.env.TOOL_PROFILE = 'dupes';
|
|
370
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
|
|
371
|
+
expect(result.tools.size).toBe(2);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|