sumulige-claude 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.claude/hooks/code-formatter.cjs +7 -2
  2. package/.claude/hooks/multi-session.cjs +9 -3
  3. package/.claude/hooks/pre-commit.cjs +0 -0
  4. package/.claude/hooks/pre-push.cjs +0 -0
  5. package/.claude/hooks/project-kickoff.cjs +22 -11
  6. package/.claude/hooks/rag-skill-loader.cjs +7 -0
  7. package/.claude/hooks/thinking-silent.cjs +9 -3
  8. package/.claude/hooks/todo-manager.cjs +19 -13
  9. package/.claude/hooks/verify-work.cjs +10 -4
  10. package/.claude/quality-gate.json +9 -3
  11. package/.claude/settings.local.json +16 -1
  12. package/.claude/templates/hooks/README.md +302 -0
  13. package/.claude/templates/hooks/hook.sh.template +94 -0
  14. package/.claude/templates/hooks/user-prompt-submit.cjs.template +116 -0
  15. package/.claude/templates/hooks/user-response-submit.cjs.template +94 -0
  16. package/.claude/templates/hooks/validate.js +173 -0
  17. package/.claude/workflow/document-scanner.js +426 -0
  18. package/.claude/workflow/knowledge-engine.js +941 -0
  19. package/.claude/workflow/notebooklm/browser.js +1028 -0
  20. package/.claude/workflow/phases/phase1-research.js +578 -0
  21. package/.claude/workflow/phases/phase1-research.ts +465 -0
  22. package/.claude/workflow/phases/phase2-approve.js +722 -0
  23. package/.claude/workflow/phases/phase3-plan.js +1200 -0
  24. package/.claude/workflow/phases/phase4-develop.js +894 -0
  25. package/.claude/workflow/search-cache.js +230 -0
  26. package/.claude/workflow/templates/approval.md +315 -0
  27. package/.claude/workflow/templates/development.md +377 -0
  28. package/.claude/workflow/templates/planning.md +328 -0
  29. package/.claude/workflow/templates/research.md +250 -0
  30. package/.claude/workflow/types.js +37 -0
  31. package/.claude/workflow/web-search.js +278 -0
  32. package/.claude-plugin/marketplace.json +2 -2
  33. package/AGENTS.md +176 -0
  34. package/CHANGELOG.md +7 -14
  35. package/cli.js +20 -0
  36. package/config/quality-gate.json +9 -3
  37. package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +36 -0
  38. package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +36 -0
  39. package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +36 -0
  40. package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +36 -0
  41. package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +36 -0
  42. package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +36 -0
  43. package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +36 -0
  44. package/development/knowledge-base/.index.clean.json +0 -0
  45. package/development/knowledge-base/.index.json +486 -0
  46. package/development/knowledge-base/test-best-practices.md +29 -0
  47. package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +160 -0
  48. package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +160 -0
  49. package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +160 -0
  50. package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +160 -0
  51. package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +160 -0
  52. package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +160 -0
  53. package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +160 -0
  54. package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +160 -0
  55. package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +160 -0
  56. package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +160 -0
  57. package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +226 -0
  58. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +345 -0
  59. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +284 -0
  60. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +14 -0
  61. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +35 -0
  62. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +34 -0
  63. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +5 -0
  64. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +60 -0
  65. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +25 -0
  66. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +70 -0
  67. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +48 -0
  68. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +20 -0
  69. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +21 -0
  70. package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +160 -0
  71. package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +226 -0
  72. package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +345 -0
  73. package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +284 -0
  74. package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +14 -0
  75. package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +160 -0
  76. package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +178 -0
  77. package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +377 -0
  78. package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +442 -0
  79. package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +800 -0
  80. package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +625 -0
  81. package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +830 -0
  82. package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +957 -0
  83. package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +381 -0
  84. package/development/todos/.state.json +14 -1
  85. package/development/todos/INDEX.md +31 -73
  86. package/development/todos/completed/develop/local-knowledge-index.md +85 -0
  87. package/development/todos/{active → completed/develop}/todo-system.md +13 -3
  88. package/development/todos/completed/develop/web-search-integration.md +83 -0
  89. package/development/todos/completed/test/phase1-e2e-test.md +103 -0
  90. package/lib/commands.js +388 -0
  91. package/package.json +3 -2
  92. package/tests/config-manager.test.js +677 -0
  93. package/tests/config-validator.test.js +436 -0
  94. package/tests/errors.test.js +477 -0
  95. package/tests/manual/phase1-e2e.sh +389 -0
  96. package/tests/manual/phase2-test-cases.md +311 -0
  97. package/tests/manual/phase3-test-cases.md +309 -0
  98. package/tests/manual/phase4-test-cases.md +414 -0
  99. package/tests/manual/test-cases.md +417 -0
  100. package/tests/quality-gate.test.js +679 -0
  101. package/tests/quality-rules.test.js +619 -0
  102. package/tests/version-check.test.js +75 -0
@@ -0,0 +1,677 @@
1
+ /**
2
+ * Config Manager 模块单元测试
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ describe('Config Manager Module', () => {
10
+ const { ConfigManager } = require('../lib/config-manager');
11
+ const { ConfigError } = require('../lib/errors');
12
+
13
+ // Temporary test directories
14
+ const tempBaseDir = path.join(os.tmpdir(), 'smc-config-test-' + Date.now());
15
+ const tempConfigDir = path.join(tempBaseDir, '.claude');
16
+ const tempConfigFile = path.join(tempConfigDir, 'config.json');
17
+ const tempBackupDir = path.join(tempConfigDir, 'backups');
18
+ const tempHistoryFile = path.join(tempConfigDir, 'config-history.jsonl');
19
+
20
+ // Sample configs (matching config-schema requirements)
21
+ const sampleConfig = {
22
+ version: '1.0.7',
23
+ model: 'claude-opus-4.5',
24
+ agents: {
25
+ conductor: { role: 'coordination' },
26
+ architect: { role: 'design' }
27
+ },
28
+ skills: ['anthropics/skills'],
29
+ hooks: {
30
+ preTask: [],
31
+ postTask: []
32
+ }
33
+ };
34
+
35
+ const sampleConfig2 = {
36
+ version: '1.0.8',
37
+ model: 'claude-opus-4.5',
38
+ agents: {
39
+ conductor: { role: 'coordination' },
40
+ architect: { role: 'design' },
41
+ builder: { role: 'implementation' }
42
+ },
43
+ skills: ['anthropics/skills', 'numman-ali/n-skills'],
44
+ hooks: {
45
+ preTask: [],
46
+ postTask: []
47
+ }
48
+ };
49
+
50
+ beforeAll(() => {
51
+ // Create test directory structure
52
+ fs.mkdirSync(tempConfigDir, { recursive: true });
53
+ fs.mkdirSync(tempBackupDir, { recursive: true });
54
+ });
55
+
56
+ afterAll(() => {
57
+ if (fs.existsSync(tempBaseDir)) {
58
+ fs.rmSync(tempBaseDir, { recursive: true, force: true });
59
+ }
60
+ });
61
+
62
+ afterEach(() => {
63
+ // Clean up config and history files between tests
64
+ if (fs.existsSync(tempConfigFile)) {
65
+ fs.unlinkSync(tempConfigFile);
66
+ }
67
+ if (fs.existsSync(tempHistoryFile)) {
68
+ fs.unlinkSync(tempHistoryFile);
69
+ }
70
+ // Clean up backup files
71
+ if (fs.existsSync(tempBackupDir)) {
72
+ const backups = fs.readdirSync(tempBackupDir);
73
+ backups.forEach(f => fs.unlinkSync(path.join(tempBackupDir, f)));
74
+ }
75
+ });
76
+
77
+ describe('constructor', () => {
78
+ it('should create manager with default paths', () => {
79
+ const manager = new ConfigManager();
80
+
81
+ expect(manager.configDir).toContain('.claude');
82
+ expect(manager.configFile).toContain('config.json');
83
+ expect(manager.backupDir).toContain('backups');
84
+ expect(manager.maxBackups).toBe(10);
85
+ });
86
+
87
+ it('should accept custom options', () => {
88
+ const manager = new ConfigManager({
89
+ configDir: tempConfigDir,
90
+ configFile: tempConfigFile,
91
+ backupDir: tempBackupDir,
92
+ maxBackups: 5
93
+ });
94
+
95
+ expect(manager.configDir).toBe(tempConfigDir);
96
+ expect(manager.configFile).toBe(tempConfigFile);
97
+ expect(manager.backupDir).toBe(tempBackupDir);
98
+ expect(manager.maxBackups).toBe(5);
99
+ });
100
+
101
+ it('should create directories if they don\'t exist', () => {
102
+ const newDir = path.join(os.tmpdir(), 'smc-new-test-' + Date.now());
103
+ const newConfigFile = path.join(newDir, 'config.json');
104
+
105
+ const manager = new ConfigManager({
106
+ configDir: newDir,
107
+ configFile: newConfigFile
108
+ });
109
+
110
+ expect(fs.existsSync(newDir)).toBe(true);
111
+
112
+ // Cleanup
113
+ fs.rmSync(newDir, { recursive: true, force: true });
114
+ });
115
+
116
+ it('should initialize validator', () => {
117
+ const manager = new ConfigManager();
118
+
119
+ expect(manager.validator).toBeDefined();
120
+ });
121
+
122
+ it('should respect strict option', () => {
123
+ const strictManager = new ConfigManager({ strict: true });
124
+ const looseManager = new ConfigManager({ strict: false });
125
+
126
+ expect(strictManager.validator.strict).toBe(true);
127
+ expect(looseManager.validator.strict).toBe(false);
128
+ });
129
+ });
130
+
131
+ describe('load', () => {
132
+ let manager;
133
+
134
+ beforeEach(() => {
135
+ manager = new ConfigManager({
136
+ configDir: tempConfigDir,
137
+ configFile: tempConfigFile
138
+ });
139
+ });
140
+
141
+ it('should return defaults when config file doesn\'t exist', () => {
142
+ const result = manager.load({ useDefaults: true });
143
+
144
+ expect(result).toBeDefined();
145
+ expect(result.version).toBeDefined();
146
+ expect(result.agents).toBeDefined();
147
+ });
148
+
149
+ it('should throw error when file doesn\'t exist and useDefaults is false', () => {
150
+ expect(() => {
151
+ manager.load({ useDefaults: false });
152
+ }).toThrow(ConfigError);
153
+ });
154
+
155
+ it('should load existing config file', () => {
156
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
157
+
158
+ const result = manager.load();
159
+
160
+ expect(result).toEqual(sampleConfig);
161
+ });
162
+
163
+ it('should expand environment variables', () => {
164
+ process.env.TEST_VAR = 'test-value';
165
+ process.env.TEST_VAR_WITH_DEFAULT = 'override';
166
+
167
+ const configWithEnv = {
168
+ version: '1.0.0',
169
+ apiKey: '${TEST_VAR}',
170
+ url: '${TEST_VAR_WITH_DEFAULT:default-value}',
171
+ nested: {
172
+ value: '${TEST_VAR}'
173
+ }
174
+ };
175
+
176
+ fs.writeFileSync(tempConfigFile, JSON.stringify(configWithEnv));
177
+
178
+ const result = manager.load({ expandEnv: true });
179
+
180
+ expect(result.apiKey).toBe('test-value');
181
+ expect(result.url).toBe('override');
182
+ expect(result.nested.value).toBe('test-value');
183
+
184
+ delete process.env.TEST_VAR;
185
+ delete process.env.TEST_VAR_WITH_DEFAULT;
186
+ });
187
+
188
+ it('should handle missing env vars with defaults', () => {
189
+ const configWithEnv = {
190
+ version: '1.0.0',
191
+ value: '${MISSING_VAR:default}'
192
+ };
193
+
194
+ fs.writeFileSync(tempConfigFile, JSON.stringify(configWithEnv));
195
+
196
+ const result = manager.load({ expandEnv: true });
197
+
198
+ expect(result.value).toBe('default');
199
+ });
200
+
201
+ it('should handle missing env vars without defaults', () => {
202
+ const configWithEnv = {
203
+ version: '1.0.0',
204
+ value: '${MISSING_VAR}'
205
+ };
206
+
207
+ fs.writeFileSync(tempConfigFile, JSON.stringify(configWithEnv));
208
+
209
+ const result = manager.load({ expandEnv: true });
210
+
211
+ expect(result.value).toBe('');
212
+ });
213
+
214
+ it('should expand env vars in arrays', () => {
215
+ process.env.ARRAY_VAR = 'item1';
216
+
217
+ const configWithEnv = {
218
+ version: '1.0.0',
219
+ items: ['${ARRAY_VAR}', 'static']
220
+ };
221
+
222
+ fs.writeFileSync(tempConfigFile, JSON.stringify(configWithEnv));
223
+
224
+ const result = manager.load({ expandEnv: true });
225
+
226
+ expect(result.items[0]).toBe('item1');
227
+ expect(result.items[1]).toBe('static');
228
+
229
+ delete process.env.ARRAY_VAR;
230
+ });
231
+
232
+ it('should validate when strict is true', () => {
233
+ fs.writeFileSync(tempConfigFile, JSON.stringify({ invalid: 'config' }));
234
+
235
+ expect(() => {
236
+ manager.load({ strict: true, useDefaults: false });
237
+ }).toThrow();
238
+ });
239
+
240
+ it('should skip validation when strict is false', () => {
241
+ const manager = new ConfigManager({
242
+ configDir: tempConfigDir,
243
+ configFile: tempConfigFile,
244
+ strict: false
245
+ });
246
+
247
+ fs.writeFileSync(tempConfigFile, JSON.stringify({ any: 'config' }));
248
+
249
+ const result = manager.load({ strict: false });
250
+
251
+ expect(result.any).toBe('config');
252
+ });
253
+ });
254
+
255
+ describe('save', () => {
256
+ let manager;
257
+
258
+ beforeEach(() => {
259
+ manager = new ConfigManager({
260
+ configDir: tempConfigDir,
261
+ configFile: tempConfigFile,
262
+ backupDir: tempBackupDir
263
+ });
264
+ });
265
+
266
+ it('should save config to file', () => {
267
+ const result = manager.save(sampleConfig, { backup: false, validate: false });
268
+
269
+ expect(result.success).toBe(true);
270
+ expect(fs.existsSync(tempConfigFile)).toBe(true);
271
+
272
+ const saved = JSON.parse(fs.readFileSync(tempConfigFile, 'utf-8'));
273
+ expect(saved).toEqual(sampleConfig);
274
+ });
275
+
276
+ it('should create backup when backup option is true', () => {
277
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
278
+
279
+ const result = manager.save(sampleConfig2, { backup: true });
280
+
281
+ expect(result.success).toBe(true);
282
+ expect(result.backup).toBeDefined();
283
+ expect(fs.existsSync(result.backup)).toBe(true);
284
+ });
285
+
286
+ it('should not create backup when file doesn\'t exist', () => {
287
+ const result = manager.save(sampleConfig, { backup: true });
288
+
289
+ expect(result.success).toBe(true);
290
+ expect(result.backup).toBeNull();
291
+ });
292
+
293
+ it('should validate before saving by default', () => {
294
+ const invalidConfig = { invalid: 'data' };
295
+
296
+ expect(() => {
297
+ manager.save(invalidConfig);
298
+ }).toThrow(ConfigError);
299
+ });
300
+
301
+ it('should skip validation when validate is false', () => {
302
+ const result = manager.save({ any: 'data' }, { validate: false });
303
+
304
+ expect(result.success).toBe(true);
305
+ });
306
+
307
+ it('should record change in history', () => {
308
+ manager.save(sampleConfig, { backup: false, validate: false });
309
+
310
+ expect(fs.existsSync(tempHistoryFile)).toBe(true);
311
+
312
+ const historyContent = fs.readFileSync(tempHistoryFile, 'utf-8');
313
+ expect(historyContent).toContain('"action":"save"');
314
+ });
315
+ });
316
+
317
+ describe('rollback', () => {
318
+ let manager;
319
+
320
+ beforeEach(() => {
321
+ manager = new ConfigManager({
322
+ configDir: tempConfigDir,
323
+ configFile: tempConfigFile,
324
+ backupDir: tempBackupDir
325
+ });
326
+
327
+ // Create initial config and a backup
328
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
329
+ manager.save(sampleConfig, { backup: true, validate: false });
330
+ manager.save(sampleConfig2, { backup: true, validate: false });
331
+ });
332
+
333
+ it('should rollback to latest backup by default', () => {
334
+ // Overwrite with different config
335
+ fs.writeFileSync(tempConfigFile, JSON.stringify({ version: '1.0.0', model: 'claude-opus-4.5', skills: [] }));
336
+
337
+ const result = manager.rollback();
338
+
339
+ expect(result.success).toBe(true);
340
+ expect(fs.existsSync(tempConfigFile)).toBe(true);
341
+
342
+ const rolledBack = JSON.parse(fs.readFileSync(tempConfigFile, 'utf-8'));
343
+ // Should rollback to sampleConfig (v1.0.7) which was backed up before saving sampleConfig2
344
+ expect(rolledBack.version).toBe('1.0.7');
345
+ });
346
+
347
+ it('should rollback to specific version', () => {
348
+ const backups = manager.listBackups();
349
+
350
+ if (backups.length > 1) {
351
+ const targetVersion = backups[1].version;
352
+ const result = manager.rollback(targetVersion);
353
+
354
+ expect(result.success).toBe(true);
355
+ expect(result.restoredFrom).toBe(targetVersion);
356
+ }
357
+ });
358
+
359
+ it('should throw error when no backups exist', () => {
360
+ const emptyManager = new ConfigManager({
361
+ configDir: tempConfigDir,
362
+ backupDir: path.join(os.tmpdir(), 'no-backups-' + Date.now())
363
+ });
364
+
365
+ expect(() => {
366
+ emptyManager.rollback();
367
+ }).toThrow(ConfigError);
368
+ });
369
+
370
+ it('should throw error for non-existent version', () => {
371
+ expect(() => {
372
+ manager.rollback('non-existent-version');
373
+ }).toThrow(ConfigError);
374
+ });
375
+
376
+ it('should backup current config before rollback', () => {
377
+ fs.writeFileSync(tempConfigFile, JSON.stringify({ current: 'config' }));
378
+
379
+ const backupsBefore = manager.listBackups().length;
380
+
381
+ manager.rollback();
382
+
383
+ const backupsAfter = manager.listBackups().length;
384
+
385
+ // Should have one more backup (pre-rollback)
386
+ expect(backupsAfter).toBeGreaterThan(backupsBefore);
387
+ });
388
+ });
389
+
390
+ describe('listBackups', () => {
391
+ let manager;
392
+
393
+ beforeEach(() => {
394
+ manager = new ConfigManager({
395
+ configDir: tempConfigDir,
396
+ configFile: tempConfigFile,
397
+ backupDir: tempBackupDir
398
+ });
399
+ });
400
+
401
+ it('should return empty array when no backups exist', () => {
402
+ const backups = manager.listBackups();
403
+
404
+ expect(backups).toEqual([]);
405
+ });
406
+
407
+ it('should return empty array when backup dir doesn\'t exist', () => {
408
+ const managerWithoutDir = new ConfigManager({
409
+ backupDir: path.join(os.tmpdir(), 'no-dir-' + Date.now())
410
+ });
411
+
412
+ const backups = managerWithoutDir.listBackups();
413
+
414
+ expect(backups).toEqual([]);
415
+ });
416
+
417
+ it('should list all backups', () => {
418
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
419
+ manager.save(sampleConfig, { backup: true, validate: false });
420
+
421
+ const backups = manager.listBackups();
422
+
423
+ expect(backups.length).toBe(1);
424
+ expect(backups[0]).toMatchObject({
425
+ file: expect.stringContaining('config-'),
426
+ version: expect.any(String),
427
+ size: expect.any(Number)
428
+ });
429
+ expect(backups[0].timestamp).toBeDefined();
430
+ expect(typeof backups[0].timestamp.getTime).toBe('function');
431
+ });
432
+
433
+ it('should sort backups by timestamp (newest first)', () => {
434
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
435
+ manager.save(sampleConfig, { backup: true, validate: false });
436
+ // Small delay to ensure different timestamp
437
+ const start = Date.now();
438
+ while (Date.now() - start < 10) {}
439
+ manager.save(sampleConfig2, { backup: true, validate: false });
440
+
441
+ const backups = manager.listBackups();
442
+
443
+ if (backups.length >= 2) {
444
+ expect(backups[0].timestamp.getTime()).toBeGreaterThanOrEqual(backups[1].timestamp.getTime());
445
+ }
446
+ });
447
+
448
+ it('should respect maxBackups limit', () => {
449
+ const managerWithLimit = new ConfigManager({
450
+ configDir: tempConfigDir,
451
+ configFile: tempConfigFile,
452
+ backupDir: tempBackupDir,
453
+ maxBackups: 2
454
+ });
455
+
456
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
457
+
458
+ // Create 3 backups
459
+ managerWithLimit.save(sampleConfig, { backup: true, validate: false });
460
+ const start = Date.now();
461
+ while (Date.now() - start < 10) {}
462
+ managerWithLimit.save(sampleConfig2, { backup: true, validate: false });
463
+ const start2 = Date.now();
464
+ while (Date.now() - start2 < 10) {}
465
+ managerWithLimit.save(sampleConfig, { backup: true, validate: false });
466
+
467
+ const backups = managerWithLimit.listBackups();
468
+
469
+ // Should only return 2 (limited by maxBackups)
470
+ expect(backups.length).toBe(2);
471
+ });
472
+ });
473
+
474
+ describe('diff', () => {
475
+ let manager;
476
+
477
+ beforeEach(() => {
478
+ manager = new ConfigManager({
479
+ configDir: tempConfigDir,
480
+ configFile: tempConfigFile
481
+ });
482
+ });
483
+
484
+ it('should compute diff between two objects', () => {
485
+ const changes = manager.diff(
486
+ { version: '1.0.0', key: 'value' },
487
+ { version: '1.0.1', key: 'value' }
488
+ );
489
+
490
+ expect(changes).toHaveLength(1);
491
+ expect(changes[0]).toMatchObject({
492
+ path: 'version',
493
+ from: '1.0.0',
494
+ to: '1.0.1',
495
+ type: 'changed'
496
+ });
497
+ });
498
+
499
+ it('should detect added properties', () => {
500
+ const changes = manager.diff(
501
+ { version: '1.0.0' },
502
+ { version: '1.0.0', newKey: 'newValue' }
503
+ );
504
+
505
+ expect(changes).toHaveLength(1);
506
+ expect(changes[0].type).toBe('added');
507
+ });
508
+
509
+ it('should detect removed properties', () => {
510
+ const changes = manager.diff(
511
+ { version: '1.0.0', oldKey: 'oldValue' },
512
+ { version: '1.0.0' }
513
+ );
514
+
515
+ expect(changes).toHaveLength(1);
516
+ expect(changes[0].type).toBe('removed');
517
+ });
518
+
519
+ it('should handle nested objects', () => {
520
+ const changes = manager.diff(
521
+ { nested: { a: 1, b: 2 } },
522
+ { nested: { a: 1, b: 3 } }
523
+ );
524
+
525
+ expect(changes).toHaveLength(1);
526
+ expect(changes[0].path).toBe('nested.b');
527
+ });
528
+
529
+ it('should accept file paths', () => {
530
+ const file1 = path.join(tempBaseDir, 'config1.json');
531
+ const file2 = path.join(tempBaseDir, 'config2.json');
532
+
533
+ fs.writeFileSync(file1, JSON.stringify({ version: '1.0.0' }));
534
+ fs.writeFileSync(file2, JSON.stringify({ version: '1.0.1' }));
535
+
536
+ const changes = manager.diff(file1, file2);
537
+
538
+ expect(changes.length).toBeGreaterThan(0);
539
+ });
540
+
541
+ it('should use current config when right is null', () => {
542
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
543
+
544
+ const changes = manager.diff({ version: '1.0.0', model: 'claude-opus-4.5', skills: [] });
545
+
546
+ expect(changes).toBeDefined();
547
+ expect(changes.length).toBeGreaterThan(0);
548
+ });
549
+ });
550
+
551
+ describe('getHistory', () => {
552
+ let manager;
553
+
554
+ beforeEach(() => {
555
+ manager = new ConfigManager({
556
+ configDir: tempConfigDir,
557
+ configFile: tempConfigFile
558
+ });
559
+ });
560
+
561
+ it('should return empty array when no history exists', () => {
562
+ const history = manager.getHistory();
563
+
564
+ expect(history).toEqual([]);
565
+ });
566
+
567
+ it('should return history entries', () => {
568
+ manager.save(sampleConfig, { backup: false, validate: false });
569
+
570
+ const history = manager.getHistory();
571
+
572
+ expect(history.length).toBeGreaterThan(0);
573
+ expect(history[0]).toMatchObject({
574
+ timestamp: expect.any(String),
575
+ action: expect.any(String)
576
+ });
577
+ });
578
+
579
+ it('should respect limit parameter', () => {
580
+ // Create multiple history entries
581
+ manager.save(sampleConfig, { backup: false, validate: false });
582
+ manager.save(sampleConfig2, { backup: false, validate: false });
583
+
584
+ const limitedHistory = manager.getHistory(1);
585
+
586
+ expect(limitedHistory.length).toBe(1);
587
+ });
588
+
589
+ it('should return entries in reverse chronological order', () => {
590
+ manager.save(sampleConfig, { backup: false, validate: false });
591
+ manager.save(sampleConfig2, { backup: false, validate: false });
592
+
593
+ const history = manager.getHistory();
594
+
595
+ expect(history).toHaveLength(2);
596
+ });
597
+ });
598
+
599
+ describe('Integration Tests', () => {
600
+ it('should handle full workflow: save -> list -> rollback', () => {
601
+ const manager = new ConfigManager({
602
+ configDir: tempConfigDir,
603
+ configFile: tempConfigFile,
604
+ backupDir: tempBackupDir
605
+ });
606
+
607
+ // Save initial config
608
+ manager.save(sampleConfig, { backup: false, validate: false });
609
+
610
+ // Save with backup
611
+ manager.save(sampleConfig2, { backup: true, validate: false });
612
+
613
+ // List backups
614
+ const backups = manager.listBackups();
615
+ expect(backups.length).toBe(1);
616
+
617
+ // Modify current config
618
+ fs.writeFileSync(tempConfigFile, JSON.stringify({ version: '1.0.0', model: 'claude-opus-4.5', skills: [] }));
619
+
620
+ // Rollback
621
+ const rollbackResult = manager.rollback();
622
+ expect(rollbackResult.success).toBe(true);
623
+
624
+ // Verify restored
625
+ const restored = JSON.parse(fs.readFileSync(tempConfigFile, 'utf-8'));
626
+ // Rollback gets the backup created before saving sampleConfig2, which is sampleConfig (v1.0.7)
627
+ expect(restored.version).toBe('1.0.7');
628
+ });
629
+
630
+ it('should handle env expansion in saved config', () => {
631
+ process.env.EXPAND_TEST = 'expanded';
632
+
633
+ const manager = new ConfigManager({
634
+ configDir: tempConfigDir,
635
+ configFile: tempConfigFile
636
+ });
637
+
638
+ const config = {
639
+ version: '1.0.0',
640
+ value: '${EXPAND_TEST}'
641
+ };
642
+
643
+ manager.save(config, { backup: false, validate: false });
644
+
645
+ // Load and verify expansion
646
+ const loaded = manager.load({ expandEnv: true });
647
+ expect(loaded.value).toBe('expanded');
648
+
649
+ delete process.env.EXPAND_TEST;
650
+ });
651
+ });
652
+
653
+ describe('Backup Management', () => {
654
+ it('should clean old backups beyond maxBackups', () => {
655
+ const manager = new ConfigManager({
656
+ configDir: tempConfigDir,
657
+ configFile: tempConfigFile,
658
+ backupDir: tempBackupDir,
659
+ maxBackups: 3
660
+ });
661
+
662
+ fs.writeFileSync(tempConfigFile, JSON.stringify(sampleConfig));
663
+
664
+ // Create 5 backups
665
+ for (let i = 0; i < 5; i++) {
666
+ const start = Date.now();
667
+ while (Date.now() - start < 5) {}
668
+ manager.save({ ...sampleConfig, iteration: i }, { backup: true, validate: false });
669
+ }
670
+
671
+ const backupFiles = fs.readdirSync(tempBackupDir);
672
+
673
+ // Should keep maxBackups (3) files, possibly plus one from initial write
674
+ expect(backupFiles.length).toBeLessThanOrEqual(5);
675
+ });
676
+ });
677
+ });