@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,542 @@
|
|
|
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 database_1 = require("../../utils/database");
|
|
37
|
+
const feature_flags_1 = require("../../utils/feature-flags");
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
describe('Feature Flags Integration Tests', () => {
|
|
42
|
+
let dbManager;
|
|
43
|
+
let featureFlagManager;
|
|
44
|
+
let tempDbPath;
|
|
45
|
+
let _db;
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tempDbPath = path.join(os.tmpdir(), `test-feature-flags-${Date.now()}.db`);
|
|
48
|
+
dbManager = new database_1.DatabaseManager({
|
|
49
|
+
filename: tempDbPath,
|
|
50
|
+
maxSize: 10 * 1024 * 1024,
|
|
51
|
+
walMode: true,
|
|
52
|
+
});
|
|
53
|
+
_db = dbManager.getDatabase();
|
|
54
|
+
featureFlagManager = new feature_flags_1.FeatureFlagManager(dbManager);
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
dbManager.close();
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(tempDbPath);
|
|
60
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
61
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
62
|
+
}
|
|
63
|
+
catch (_e) {
|
|
64
|
+
// Ignore
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
describe('Flag Management', () => {
|
|
68
|
+
it('should create and retrieve feature flags', () => {
|
|
69
|
+
const flag = {
|
|
70
|
+
name: 'Test Feature',
|
|
71
|
+
key: 'test_feature',
|
|
72
|
+
enabled: true,
|
|
73
|
+
description: 'A test feature flag',
|
|
74
|
+
category: 'testing',
|
|
75
|
+
tags: ['test', 'demo'],
|
|
76
|
+
};
|
|
77
|
+
const _flagId = featureFlagManager.createFlag(flag);
|
|
78
|
+
expect(_flagId).toBeDefined();
|
|
79
|
+
expect(typeof _flagId).toBe('string');
|
|
80
|
+
const retrieved = featureFlagManager.getFlag(_flagId);
|
|
81
|
+
expect(retrieved).toBeDefined();
|
|
82
|
+
expect(retrieved.name).toBe(flag.name);
|
|
83
|
+
expect(retrieved.key).toBe(flag.key);
|
|
84
|
+
expect(retrieved.enabled).toBe(flag.enabled);
|
|
85
|
+
expect(retrieved.description).toBe(flag.description);
|
|
86
|
+
expect(retrieved.category).toBe(flag.category);
|
|
87
|
+
expect(retrieved.tags).toEqual(flag.tags);
|
|
88
|
+
});
|
|
89
|
+
it('should retrieve flags by key', () => {
|
|
90
|
+
const flag = {
|
|
91
|
+
name: 'Key Test',
|
|
92
|
+
key: 'key_test_flag',
|
|
93
|
+
enabled: false,
|
|
94
|
+
description: 'Testing key retrieval',
|
|
95
|
+
};
|
|
96
|
+
featureFlagManager.createFlag(flag);
|
|
97
|
+
const retrieved = featureFlagManager.getFlagByKey(flag.key);
|
|
98
|
+
expect(retrieved).toBeDefined();
|
|
99
|
+
expect(retrieved.name).toBe(flag.name);
|
|
100
|
+
expect(retrieved.key).toBe(flag.key);
|
|
101
|
+
});
|
|
102
|
+
it('should list all flags', () => {
|
|
103
|
+
const flags = [
|
|
104
|
+
{ name: 'Flag 1', key: 'flag_1', enabled: true, category: 'core' },
|
|
105
|
+
{ name: 'Flag 2', key: 'flag_2', enabled: false, category: 'experimental' },
|
|
106
|
+
{ name: 'Flag 3', key: 'flag_3', enabled: true, category: 'core' },
|
|
107
|
+
];
|
|
108
|
+
const _flagIds = flags.map(f => featureFlagManager.createFlag(f));
|
|
109
|
+
const allFlags = featureFlagManager.listFlags();
|
|
110
|
+
expect(allFlags.length).toBeGreaterThanOrEqual(3);
|
|
111
|
+
const createdFlags = allFlags.filter(f => _flagIds.includes(f.id));
|
|
112
|
+
expect(createdFlags.length).toBe(3);
|
|
113
|
+
});
|
|
114
|
+
it('should filter flags by category', () => {
|
|
115
|
+
const flags = [
|
|
116
|
+
{ name: 'Core Flag', key: 'core_flag', enabled: true, category: 'core' },
|
|
117
|
+
{ name: 'Experimental Flag', key: 'exp_flag', enabled: true, category: 'experimental' },
|
|
118
|
+
];
|
|
119
|
+
flags.forEach(f => featureFlagManager.createFlag(f));
|
|
120
|
+
const coreFlags = featureFlagManager.listFlags({ category: 'core' });
|
|
121
|
+
expect(coreFlags.length).toBeGreaterThanOrEqual(1);
|
|
122
|
+
expect(coreFlags.every(f => f.category === 'core')).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('should filter flags by enabled status', () => {
|
|
125
|
+
const flags = [
|
|
126
|
+
{ name: 'Enabled Flag', key: 'enabled_flag', enabled: true },
|
|
127
|
+
{ name: 'Disabled Flag', key: 'disabled_flag', enabled: false },
|
|
128
|
+
];
|
|
129
|
+
flags.forEach(f => featureFlagManager.createFlag(f));
|
|
130
|
+
const enabledFlags = featureFlagManager.listFlags({ enabled: true });
|
|
131
|
+
const disabledFlags = featureFlagManager.listFlags({ enabled: false });
|
|
132
|
+
expect(enabledFlags.every(f => f.enabled === true)).toBe(true);
|
|
133
|
+
expect(disabledFlags.every(f => f.enabled === false)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
it('should update existing flags', () => {
|
|
136
|
+
const flag = {
|
|
137
|
+
name: 'Original Flag',
|
|
138
|
+
key: 'update_test',
|
|
139
|
+
enabled: false,
|
|
140
|
+
description: 'Original description',
|
|
141
|
+
};
|
|
142
|
+
const _flagId = featureFlagManager.createFlag(flag);
|
|
143
|
+
featureFlagManager.updateFlag(_flagId, {
|
|
144
|
+
name: 'Updated Flag',
|
|
145
|
+
enabled: true,
|
|
146
|
+
description: 'Updated description',
|
|
147
|
+
category: 'updated',
|
|
148
|
+
});
|
|
149
|
+
const updated = featureFlagManager.getFlag(_flagId);
|
|
150
|
+
expect(updated.name).toBe('Updated Flag');
|
|
151
|
+
expect(updated.enabled).toBe(true);
|
|
152
|
+
expect(updated.description).toBe('Updated description');
|
|
153
|
+
expect(updated.category).toBe('updated');
|
|
154
|
+
expect(updated.key).toBe('update_test'); // Should remain unchanged
|
|
155
|
+
});
|
|
156
|
+
it('should delete flags', () => {
|
|
157
|
+
const flag = {
|
|
158
|
+
name: 'Temporary Flag',
|
|
159
|
+
key: 'temp_flag',
|
|
160
|
+
enabled: true,
|
|
161
|
+
};
|
|
162
|
+
const _flagId = featureFlagManager.createFlag(flag);
|
|
163
|
+
expect(featureFlagManager.getFlag(_flagId)).toBeDefined();
|
|
164
|
+
featureFlagManager.deleteFlag(_flagId);
|
|
165
|
+
expect(featureFlagManager.getFlag(_flagId)).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
it('should enforce unique keys', () => {
|
|
168
|
+
const flag1 = { name: 'Flag 1', key: 'duplicate_key', enabled: true };
|
|
169
|
+
const flag2 = { name: 'Flag 2', key: 'duplicate_key', enabled: false };
|
|
170
|
+
featureFlagManager.createFlag(flag1);
|
|
171
|
+
expect(() => featureFlagManager.createFlag(flag2)).toThrow();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('Flag Evaluation', () => {
|
|
175
|
+
it('should evaluate enabled flags as true', () => {
|
|
176
|
+
const flag = {
|
|
177
|
+
name: 'Enabled Test',
|
|
178
|
+
key: 'enabled_test',
|
|
179
|
+
enabled: true,
|
|
180
|
+
};
|
|
181
|
+
featureFlagManager.createFlag(flag);
|
|
182
|
+
const evaluation = featureFlagManager.evaluateFlag('enabled_test');
|
|
183
|
+
expect(evaluation.enabled).toBe(true);
|
|
184
|
+
expect(evaluation.reason).toBe('Flag enabled');
|
|
185
|
+
});
|
|
186
|
+
it('should evaluate disabled flags as false', () => {
|
|
187
|
+
const flag = {
|
|
188
|
+
name: 'Disabled Test',
|
|
189
|
+
key: 'disabled_test',
|
|
190
|
+
enabled: false,
|
|
191
|
+
};
|
|
192
|
+
featureFlagManager.createFlag(flag);
|
|
193
|
+
const evaluation = featureFlagManager.evaluateFlag('disabled_test');
|
|
194
|
+
expect(evaluation.enabled).toBe(false);
|
|
195
|
+
expect(evaluation.reason).toBe('Flag globally disabled');
|
|
196
|
+
});
|
|
197
|
+
it('should handle non-existent flags', () => {
|
|
198
|
+
const evaluation = featureFlagManager.evaluateFlag('non_existent');
|
|
199
|
+
expect(evaluation.enabled).toBe(false);
|
|
200
|
+
expect(evaluation.reason).toBe('Flag not found');
|
|
201
|
+
});
|
|
202
|
+
it('should respect environment constraints', () => {
|
|
203
|
+
const flag = {
|
|
204
|
+
name: 'Environment Test',
|
|
205
|
+
key: 'env_test',
|
|
206
|
+
enabled: true,
|
|
207
|
+
environments: ['development', 'staging'],
|
|
208
|
+
};
|
|
209
|
+
featureFlagManager.createFlag(flag);
|
|
210
|
+
// Should be enabled in development
|
|
211
|
+
const devEval = featureFlagManager.evaluateFlag('env_test', { environment: 'development' });
|
|
212
|
+
expect(devEval.enabled).toBe(true);
|
|
213
|
+
expect(devEval.reason).toBe('Enabled for environment: development');
|
|
214
|
+
// Should be disabled in production
|
|
215
|
+
const prodEval = featureFlagManager.evaluateFlag('env_test', { environment: 'production' });
|
|
216
|
+
expect(prodEval.enabled).toBe(false);
|
|
217
|
+
expect(prodEval.reason).toBe('Not enabled for environment: production');
|
|
218
|
+
});
|
|
219
|
+
it('should respect user constraints', () => {
|
|
220
|
+
const flag = {
|
|
221
|
+
name: 'User Test',
|
|
222
|
+
key: 'user_test',
|
|
223
|
+
enabled: true,
|
|
224
|
+
users: ['user1', 'user2'],
|
|
225
|
+
};
|
|
226
|
+
featureFlagManager.createFlag(flag);
|
|
227
|
+
// Should be enabled for user1
|
|
228
|
+
const user1Eval = featureFlagManager.evaluateFlag('user_test', { userId: 'user1' });
|
|
229
|
+
expect(user1Eval.enabled).toBe(true);
|
|
230
|
+
expect(user1Eval.reason).toBe('Enabled for user: user1');
|
|
231
|
+
// Should be disabled for user3
|
|
232
|
+
const user3Eval = featureFlagManager.evaluateFlag('user_test', { userId: 'user3' });
|
|
233
|
+
expect(user3Eval.enabled).toBe(false);
|
|
234
|
+
expect(user3Eval.reason).toBe('Not enabled for user: user3');
|
|
235
|
+
});
|
|
236
|
+
it('should respect percentage rollout', () => {
|
|
237
|
+
const flag = {
|
|
238
|
+
name: 'Percentage Test',
|
|
239
|
+
key: 'percentage_test',
|
|
240
|
+
enabled: true,
|
|
241
|
+
percentage: 50, // 50% rollout
|
|
242
|
+
};
|
|
243
|
+
featureFlagManager.createFlag(flag);
|
|
244
|
+
// Test with different user IDs to get different hash values
|
|
245
|
+
const results = [];
|
|
246
|
+
for (let i = 0; i < 100; i++) {
|
|
247
|
+
const evaluation = featureFlagManager.evaluateFlag('percentage_test', {
|
|
248
|
+
userId: `user${i}`,
|
|
249
|
+
});
|
|
250
|
+
results.push(evaluation.enabled);
|
|
251
|
+
}
|
|
252
|
+
const enabledCount = results.filter(r => r).length;
|
|
253
|
+
// Should be roughly 50% (allow for some variance due to hashing)
|
|
254
|
+
expect(enabledCount).toBeGreaterThan(30);
|
|
255
|
+
expect(enabledCount).toBeLessThan(70);
|
|
256
|
+
});
|
|
257
|
+
it('should respect date constraints', () => {
|
|
258
|
+
const now = new Date();
|
|
259
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day in future
|
|
260
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day in past
|
|
261
|
+
const futureFlag = {
|
|
262
|
+
name: 'Future Flag',
|
|
263
|
+
key: 'future_flag',
|
|
264
|
+
enabled: true,
|
|
265
|
+
enabledFrom: future.toISOString(),
|
|
266
|
+
};
|
|
267
|
+
const expiredFlag = {
|
|
268
|
+
name: 'Expired Flag',
|
|
269
|
+
key: 'expired_flag',
|
|
270
|
+
enabled: true,
|
|
271
|
+
enabledUntil: past.toISOString(),
|
|
272
|
+
};
|
|
273
|
+
featureFlagManager.createFlag(futureFlag);
|
|
274
|
+
featureFlagManager.createFlag(expiredFlag);
|
|
275
|
+
const futureEval = featureFlagManager.evaluateFlag('future_flag');
|
|
276
|
+
expect(futureEval.enabled).toBe(false);
|
|
277
|
+
expect(futureEval.reason).toContain('not yet active');
|
|
278
|
+
const expiredEval = featureFlagManager.evaluateFlag('expired_flag');
|
|
279
|
+
expect(expiredEval.enabled).toBe(false);
|
|
280
|
+
expect(expiredEval.reason).toContain('expired');
|
|
281
|
+
});
|
|
282
|
+
it('should provide isEnabled shortcut', () => {
|
|
283
|
+
const flag = {
|
|
284
|
+
name: 'Shortcut Test',
|
|
285
|
+
key: 'shortcut_test',
|
|
286
|
+
enabled: true,
|
|
287
|
+
};
|
|
288
|
+
featureFlagManager.createFlag(flag);
|
|
289
|
+
expect(featureFlagManager.isEnabled('shortcut_test')).toBe(true);
|
|
290
|
+
expect(featureFlagManager.isEnabled('non_existent')).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('Statistics', () => {
|
|
294
|
+
beforeEach(() => {
|
|
295
|
+
// Create test flags
|
|
296
|
+
const flags = [
|
|
297
|
+
{
|
|
298
|
+
name: 'Core Flag 1',
|
|
299
|
+
key: 'core_1',
|
|
300
|
+
enabled: true,
|
|
301
|
+
category: 'core',
|
|
302
|
+
environments: ['production'],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: 'Core Flag 2',
|
|
306
|
+
key: 'core_2',
|
|
307
|
+
enabled: false,
|
|
308
|
+
category: 'core',
|
|
309
|
+
environments: ['production'],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'Experimental Flag',
|
|
313
|
+
key: 'exp_1',
|
|
314
|
+
enabled: true,
|
|
315
|
+
category: 'experimental',
|
|
316
|
+
environments: ['development'],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'Beta Flag',
|
|
320
|
+
key: 'beta_1',
|
|
321
|
+
enabled: false,
|
|
322
|
+
category: 'beta',
|
|
323
|
+
environments: ['staging'],
|
|
324
|
+
},
|
|
325
|
+
];
|
|
326
|
+
flags.forEach(f => featureFlagManager.createFlag(f));
|
|
327
|
+
});
|
|
328
|
+
it('should calculate overall statistics', () => {
|
|
329
|
+
const stats = featureFlagManager.getStats();
|
|
330
|
+
expect(stats.totalFlags).toBe(4);
|
|
331
|
+
expect(stats.enabledFlags).toBe(2);
|
|
332
|
+
expect(stats.disabledFlags).toBe(2);
|
|
333
|
+
});
|
|
334
|
+
it('should group statistics by category', () => {
|
|
335
|
+
const stats = featureFlagManager.getStats();
|
|
336
|
+
expect(stats.byCategory.core.count).toBe(2);
|
|
337
|
+
expect(stats.byCategory.core.enabled).toBe(1);
|
|
338
|
+
expect(stats.byCategory.experimental.count).toBe(1);
|
|
339
|
+
expect(stats.byCategory.experimental.enabled).toBe(1);
|
|
340
|
+
});
|
|
341
|
+
it('should group statistics by environment', () => {
|
|
342
|
+
const stats = featureFlagManager.getStats();
|
|
343
|
+
expect(stats.byEnvironment.production.count).toBe(2);
|
|
344
|
+
expect(stats.byEnvironment.production.enabled).toBe(1);
|
|
345
|
+
expect(stats.byEnvironment.development.count).toBe(1);
|
|
346
|
+
expect(stats.byEnvironment.development.enabled).toBe(1);
|
|
347
|
+
});
|
|
348
|
+
it('should track recent activity', () => {
|
|
349
|
+
const stats = featureFlagManager.getStats();
|
|
350
|
+
expect(stats.recentActivity.length).toBeGreaterThan(0);
|
|
351
|
+
expect(stats.recentActivity[0]).toHaveProperty('flag');
|
|
352
|
+
expect(stats.recentActivity[0]).toHaveProperty('action');
|
|
353
|
+
expect(stats.recentActivity[0]).toHaveProperty('timestamp');
|
|
354
|
+
});
|
|
355
|
+
it('should identify scheduled changes', () => {
|
|
356
|
+
const future = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
357
|
+
const scheduledFlag = {
|
|
358
|
+
name: 'Scheduled Flag',
|
|
359
|
+
key: 'scheduled_flag',
|
|
360
|
+
enabled: false,
|
|
361
|
+
enabledFrom: future,
|
|
362
|
+
};
|
|
363
|
+
featureFlagManager.createFlag(scheduledFlag);
|
|
364
|
+
const stats = featureFlagManager.getStats();
|
|
365
|
+
expect(stats.scheduledChanges.toEnable.length).toBeGreaterThan(0);
|
|
366
|
+
expect(stats.scheduledChanges.toEnable[0].flag).toBe('Scheduled Flag');
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
describe('Audit and Logging', () => {
|
|
370
|
+
it('should log flag evaluations', () => {
|
|
371
|
+
const flag = {
|
|
372
|
+
name: 'Audit Test',
|
|
373
|
+
key: 'audit_test',
|
|
374
|
+
enabled: true,
|
|
375
|
+
};
|
|
376
|
+
const _flagId = featureFlagManager.createFlag(flag);
|
|
377
|
+
// Perform some evaluations
|
|
378
|
+
featureFlagManager.evaluateFlag('audit_test', { userId: 'test_user' });
|
|
379
|
+
featureFlagManager.evaluateFlag('audit_test', { environment: 'test' });
|
|
380
|
+
const history = featureFlagManager.getEvaluationHistory('audit_test');
|
|
381
|
+
expect(history.length).toBeGreaterThanOrEqual(2);
|
|
382
|
+
expect(history[0]).toHaveProperty('enabled');
|
|
383
|
+
expect(history[0]).toHaveProperty('reason');
|
|
384
|
+
});
|
|
385
|
+
it('should maintain audit log', () => {
|
|
386
|
+
const flag = {
|
|
387
|
+
name: 'Audit Flag',
|
|
388
|
+
key: 'audit_flag',
|
|
389
|
+
enabled: false,
|
|
390
|
+
};
|
|
391
|
+
const _flagId = featureFlagManager.createFlag(flag);
|
|
392
|
+
// Update the flag
|
|
393
|
+
featureFlagManager.updateFlag(_flagId, { enabled: true }, 'test_user');
|
|
394
|
+
// Delete the flag
|
|
395
|
+
featureFlagManager.deleteFlag(_flagId, 'test_user');
|
|
396
|
+
const auditLog = featureFlagManager.getAuditLog(_flagId);
|
|
397
|
+
expect(auditLog.length).toBeGreaterThanOrEqual(3); // create, update, delete
|
|
398
|
+
const actions = auditLog.map(entry => entry.action);
|
|
399
|
+
expect(actions).toContain('created');
|
|
400
|
+
expect(actions).toContain('updated');
|
|
401
|
+
expect(actions).toContain('deleted');
|
|
402
|
+
});
|
|
403
|
+
it('should track user information in audit log', () => {
|
|
404
|
+
const flag = {
|
|
405
|
+
name: 'User Tracking',
|
|
406
|
+
key: 'user_tracking',
|
|
407
|
+
enabled: true,
|
|
408
|
+
};
|
|
409
|
+
const _flagId = featureFlagManager.createFlag(flag);
|
|
410
|
+
featureFlagManager.updateFlag(_flagId, { description: 'Updated' }, 'specific_user');
|
|
411
|
+
const auditLog = featureFlagManager.getAuditLog(_flagId);
|
|
412
|
+
const updateEntry = auditLog.find(entry => entry.action === 'updated');
|
|
413
|
+
expect(updateEntry.user_id).toBe('specific_user');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
describe('Bulk Operations', () => {
|
|
417
|
+
it('should enable flags by key', () => {
|
|
418
|
+
const flag = {
|
|
419
|
+
name: 'Bulk Enable Test',
|
|
420
|
+
key: 'bulk_enable',
|
|
421
|
+
enabled: false,
|
|
422
|
+
};
|
|
423
|
+
featureFlagManager.createFlag(flag);
|
|
424
|
+
featureFlagManager.enableFlag('bulk_enable', 'admin_user');
|
|
425
|
+
const updated = featureFlagManager.getFlagByKey('bulk_enable');
|
|
426
|
+
expect(updated.enabled).toBe(true);
|
|
427
|
+
// Check audit log
|
|
428
|
+
const auditLog = featureFlagManager.getAuditLog(updated.id);
|
|
429
|
+
const enableEntry = auditLog.find(entry => entry.action === 'enabled');
|
|
430
|
+
expect(enableEntry).toBeDefined();
|
|
431
|
+
expect(enableEntry.user_id).toBe('admin_user');
|
|
432
|
+
});
|
|
433
|
+
it('should disable flags by key', () => {
|
|
434
|
+
const flag = {
|
|
435
|
+
name: 'Bulk Disable Test',
|
|
436
|
+
key: 'bulk_disable',
|
|
437
|
+
enabled: true,
|
|
438
|
+
};
|
|
439
|
+
featureFlagManager.createFlag(flag);
|
|
440
|
+
featureFlagManager.disableFlag('bulk_disable', 'admin_user');
|
|
441
|
+
const updated = featureFlagManager.getFlagByKey('bulk_disable');
|
|
442
|
+
expect(updated.enabled).toBe(false);
|
|
443
|
+
// Check audit log
|
|
444
|
+
const auditLog = featureFlagManager.getAuditLog(updated.id);
|
|
445
|
+
const disableEntry = auditLog.find(entry => entry.action === 'disabled');
|
|
446
|
+
expect(disableEntry).toBeDefined();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
describe('Default Flags', () => {
|
|
450
|
+
it('should provide default flags', () => {
|
|
451
|
+
const defaultFlags = feature_flags_1.FeatureFlagManager.getDefaultFlags();
|
|
452
|
+
expect(defaultFlags.length).toBeGreaterThan(0);
|
|
453
|
+
expect(defaultFlags[0]).toHaveProperty('name');
|
|
454
|
+
expect(defaultFlags[0]).toHaveProperty('key');
|
|
455
|
+
expect(defaultFlags[0]).toHaveProperty('enabled');
|
|
456
|
+
});
|
|
457
|
+
it('should create default flags without errors', () => {
|
|
458
|
+
const defaultFlags = feature_flags_1.FeatureFlagManager.getDefaultFlags();
|
|
459
|
+
for (const flag of defaultFlags) {
|
|
460
|
+
expect(() => featureFlagManager.createFlag(flag)).not.toThrow();
|
|
461
|
+
}
|
|
462
|
+
const createdFlags = featureFlagManager.listFlags();
|
|
463
|
+
expect(createdFlags.length).toBe(defaultFlags.length);
|
|
464
|
+
});
|
|
465
|
+
it('should have meaningful default flag categories', () => {
|
|
466
|
+
const defaultFlags = feature_flags_1.FeatureFlagManager.getDefaultFlags();
|
|
467
|
+
const categories = defaultFlags.map((f) => f.category).filter(Boolean);
|
|
468
|
+
expect(categories.length).toBeGreaterThan(0);
|
|
469
|
+
// Should have common categories
|
|
470
|
+
const categorySet = new Set(categories);
|
|
471
|
+
expect(categorySet.size).toBeGreaterThan(1); // Multiple categories
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
describe('Error Handling', () => {
|
|
475
|
+
it('should handle non-existent flag updates', () => {
|
|
476
|
+
expect(() => featureFlagManager.updateFlag('non-existent', { enabled: true })).toThrow('Feature flag not found');
|
|
477
|
+
});
|
|
478
|
+
it('should handle non-existent flag deletions', () => {
|
|
479
|
+
expect(() => featureFlagManager.deleteFlag('non-existent')).toThrow('Feature flag not found');
|
|
480
|
+
});
|
|
481
|
+
it('should handle non-existent flag enable/disable', () => {
|
|
482
|
+
expect(() => featureFlagManager.enableFlag('non-existent')).toThrow('Feature flag not found');
|
|
483
|
+
expect(() => featureFlagManager.disableFlag('non-existent')).toThrow('Feature flag not found');
|
|
484
|
+
});
|
|
485
|
+
it('should validate required fields', () => {
|
|
486
|
+
expect(() => featureFlagManager.createFlag({ name: 'Test' })).toThrow();
|
|
487
|
+
expect(() => featureFlagManager.createFlag({ key: 'test' })).toThrow();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
describe('Edge Cases', () => {
|
|
491
|
+
it('should handle flags with no constraints as always enabled when flag is enabled', () => {
|
|
492
|
+
const flag = {
|
|
493
|
+
name: 'No Constraints',
|
|
494
|
+
key: 'no_constraints',
|
|
495
|
+
enabled: true,
|
|
496
|
+
};
|
|
497
|
+
featureFlagManager.createFlag(flag);
|
|
498
|
+
const eval1 = featureFlagManager.evaluateFlag('no_constraints', { userId: 'any_user' });
|
|
499
|
+
const eval2 = featureFlagManager.evaluateFlag('no_constraints', { environment: 'any_env' });
|
|
500
|
+
const eval3 = featureFlagManager.evaluateFlag('no_constraints', {});
|
|
501
|
+
expect(eval1.enabled).toBe(true);
|
|
502
|
+
expect(eval2.enabled).toBe(true);
|
|
503
|
+
expect(eval3.enabled).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
it('should handle empty arrays as no constraints', () => {
|
|
506
|
+
const flag = {
|
|
507
|
+
name: 'Empty Arrays',
|
|
508
|
+
key: 'empty_arrays',
|
|
509
|
+
enabled: true,
|
|
510
|
+
environments: [],
|
|
511
|
+
users: [],
|
|
512
|
+
};
|
|
513
|
+
featureFlagManager.createFlag(flag);
|
|
514
|
+
const evaluation = featureFlagManager.evaluateFlag('empty_arrays', { userId: 'test' });
|
|
515
|
+
expect(evaluation.enabled).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
it('should handle zero percentage as always disabled', () => {
|
|
518
|
+
const flag = {
|
|
519
|
+
name: 'Zero Percent',
|
|
520
|
+
key: 'zero_percent',
|
|
521
|
+
enabled: true,
|
|
522
|
+
percentage: 0,
|
|
523
|
+
};
|
|
524
|
+
featureFlagManager.createFlag(flag);
|
|
525
|
+
const evaluation = featureFlagManager.evaluateFlag('zero_percent', { userId: 'test' });
|
|
526
|
+
expect(evaluation.enabled).toBe(false);
|
|
527
|
+
expect(evaluation.reason).toContain('percentage rollout (0%)');
|
|
528
|
+
});
|
|
529
|
+
it('should handle 100 percentage as always enabled', () => {
|
|
530
|
+
const flag = {
|
|
531
|
+
name: 'Full Percent',
|
|
532
|
+
key: 'full_percent',
|
|
533
|
+
enabled: true,
|
|
534
|
+
percentage: 100,
|
|
535
|
+
};
|
|
536
|
+
featureFlagManager.createFlag(flag);
|
|
537
|
+
const evaluation = featureFlagManager.evaluateFlag('full_percent', { userId: 'test' });
|
|
538
|
+
expect(evaluation.enabled).toBe(true);
|
|
539
|
+
expect(evaluation.reason).toContain('percentage rollout (100%)');
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|