sumulige-claude 1.5.1 → 1.5.2
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/.claude/hooks/hook-registry.json +0 -15
- package/.claude/rules/coding-style.md +18 -7
- package/.claude/rules/hooks.md +15 -4
- package/.claude/rules/performance.md +15 -5
- package/.claude/rules/security.md +140 -4
- package/.claude/rules/testing.md +138 -9
- package/.claude/rules/web-design-standard.md +16 -5
- package/.claude/skills/algorithmic-art/metadata.yaml +28 -0
- package/.claude/skills/api-tester/SKILL.md +61 -0
- package/.claude/skills/api-tester/examples/basic.md +3 -0
- package/.claude/skills/api-tester/metadata.yaml +30 -0
- package/.claude/skills/api-tester/templates/default.md +3 -0
- package/.claude/skills/brand-guidelines/metadata.yaml +26 -0
- package/.claude/skills/canvas-design/metadata.yaml +27 -0
- package/.claude/skills/code-reviewer-123/SKILL.md +61 -0
- package/.claude/skills/code-reviewer-123/examples/basic.md +3 -0
- package/.claude/skills/code-reviewer-123/metadata.yaml +30 -0
- package/.claude/skills/code-reviewer-123/templates/default.md +3 -0
- package/.claude/skills/doc-coauthoring/metadata.yaml +27 -0
- package/.claude/skills/docx/metadata.yaml +30 -0
- package/.claude/skills/frontend-design/metadata.yaml +28 -0
- package/.claude/skills/internal-comms/metadata.yaml +28 -0
- package/.claude/skills/mcp-builder/metadata.yaml +26 -0
- package/.claude/skills/my-skill/SKILL.md +61 -0
- package/.claude/skills/my-skill/examples/basic.md +3 -0
- package/.claude/skills/my-skill/metadata.yaml +30 -0
- package/.claude/skills/my-skill/templates/default.md +3 -0
- package/.claude/skills/pdf/metadata.yaml +29 -0
- package/.claude/skills/pptx/metadata.yaml +29 -0
- package/.claude/skills/react-best-practices/metadata.yaml +26 -0
- package/.claude/skills/react-node-practices/SKILL.md +409 -0
- package/.claude/skills/react-node-practices/metadata.yaml +56 -0
- package/.claude/skills/skill-creator/metadata.yaml +25 -0
- package/.claude/skills/slack-gif-creator/metadata.yaml +28 -0
- package/.claude/skills/test-skill-name/SKILL.md +61 -0
- package/.claude/skills/test-skill-name/examples/basic.md +3 -0
- package/.claude/skills/test-skill-name/metadata.yaml +30 -0
- package/.claude/skills/test-skill-name/templates/default.md +3 -0
- package/.claude/skills/test-workflow/metadata.yaml +32 -0
- package/.claude/skills/theme-factory/metadata.yaml +26 -0
- package/.claude/skills/threejs-fundamentals/metadata.yaml +27 -0
- package/.claude/skills/web-artifacts-builder/metadata.yaml +30 -0
- package/.claude/skills/web-design-guidelines/metadata.yaml +26 -0
- package/.claude/skills/webapp-testing/metadata.yaml +26 -0
- package/.claude/skills/xlsx/metadata.yaml +29 -0
- package/LICENSE +21 -0
- package/cli.js +1 -1
- package/package.json +25 -3
- package/.claude/.kickoff-hint.txt +0 -52
- package/.claude/.sumulige-claude-version +0 -1
- package/.claude/.version +0 -1
- package/.claude/AGENTS.md +0 -42
- package/.claude/ANCHORS.md +0 -40
- package/.claude/CLAUDE.md +0 -138
- package/.claude/MEMORY.md +0 -69
- package/.claude/PROJECT_LOG.md +0 -101
- package/.claude/THINKING_CHAIN_GUIDE.md +0 -287
- package/.claude/USAGE.md +0 -175
- package/.claude/boris-optimizations.md +0 -167
- package/.claude/handoffs/INDEX.md +0 -21
- package/.claude/handoffs/LATEST.md +0 -76
- package/.claude/handoffs/handoff_2026-01-22T13-07-04-757Z.md +0 -76
- package/.claude/quality-gate.json +0 -82
- package/.claude/rag/skill-index.json +0 -135
- package/.claude/settings.json +0 -99
- package/.claude/settings.local.json +0 -175
- package/.claude/templates/PROJECT_KICKOFF.md +0 -89
- package/.claude/templates/PROJECT_PROPOSAL.md +0 -227
- package/.claude/templates/TASK_PLAN.md +0 -121
- package/.claude/templates/hooks/README.md +0 -302
- package/.claude/templates/hooks/hook.sh.template +0 -94
- package/.claude/templates/hooks/user-prompt-submit.cjs.template +0 -116
- package/.claude/templates/hooks/user-response-submit.cjs.template +0 -94
- package/.claude/templates/hooks/validate.js +0 -173
- package/.claude/templates/tasks/develop.md +0 -69
- package/.claude/templates/tasks/research.md +0 -64
- package/.claude/templates/tasks/test.md +0 -96
- package/.claude/thinking-routes/.last-sync +0 -1
- package/.claude/thinking-routes/QUICKREF.md +0 -98
- package/.claude/workflow/document-scanner.js +0 -426
- package/.claude/workflow/knowledge-engine.js +0 -941
- package/.claude/workflow/notebooklm/browser.js +0 -1028
- package/.claude/workflow/phases/phase1-research.js +0 -578
- package/.claude/workflow/phases/phase1-research.ts +0 -465
- package/.claude/workflow/phases/phase2-approve.js +0 -722
- package/.claude/workflow/phases/phase3-plan.js +0 -1200
- package/.claude/workflow/phases/phase4-develop.js +0 -894
- package/.claude/workflow/search-cache.js +0 -230
- package/.claude/workflow/templates/approval.md +0 -315
- package/.claude/workflow/templates/development.md +0 -377
- package/.claude/workflow/templates/planning.md +0 -328
- package/.claude/workflow/templates/research.md +0 -250
- package/.claude/workflow/types.js +0 -37
- package/.claude/workflow/web-search.js +0 -278
- package/.claude-plugin/marketplace.json +0 -71
- package/.github/workflows/sync-skills.yml +0 -74
- package/.versionrc +0 -25
- package/AGENTS.md +0 -580
- package/CHANGELOG.md +0 -481
- package/CLAUDE-template.md +0 -114
- package/DEV_TOOLS_GUIDE.md +0 -190
- package/PROJECT_STRUCTURE.md +0 -266
- package/Q&A.md +0 -325
- package/config/defaults.json +0 -34
- package/config/official-skills.json +0 -183
- package/config/quality-gate.json +0 -67
- package/config/skill-categories.json +0 -40
- package/config/version-manifest.json +0 -85
- package/demos/power-3d-scatter.html +0 -683
- package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +0 -36
- package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +0 -36
- package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +0 -36
- package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +0 -36
- package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +0 -36
- package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +0 -36
- package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +0 -36
- package/development/knowledge-base/.index.clean.json +0 -1
- package/development/knowledge-base/.index.json +0 -486
- package/development/knowledge-base/test-best-practices.md +0 -29
- package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +0 -226
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +0 -345
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +0 -284
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +0 -14
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +0 -35
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +0 -34
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +0 -5
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +0 -60
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +0 -25
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +0 -70
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +0 -48
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +0 -20
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +0 -21
- package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +0 -226
- package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +0 -345
- package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +0 -284
- package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +0 -14
- package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +0 -160
- package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +0 -178
- package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +0 -377
- package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +0 -442
- package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +0 -800
- package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +0 -625
- package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +0 -830
- package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +0 -957
- package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +0 -381
- package/development/todos/.state.json +0 -19
- package/development/todos/INDEX.md +0 -63
- package/development/todos/active/_README.md +0 -49
- package/development/todos/archived/_README.md +0 -11
- package/development/todos/backlog/_README.md +0 -11
- package/development/todos/backlog/mcp-integration.md +0 -35
- package/development/todos/completed/_README.md +0 -11
- package/development/todos/completed/boris-optimizations.md +0 -39
- package/development/todos/completed/develop/local-knowledge-index.md +0 -85
- package/development/todos/completed/develop/todo-system.md +0 -47
- package/development/todos/completed/develop/web-search-integration.md +0 -83
- package/development/todos/completed/test/phase1-e2e-test.md +0 -103
- package/docs/DEVELOPMENT.md +0 -461
- package/docs/MARKETPLACE.md +0 -352
- package/docs/RELEASE.md +0 -93
- package/jest.config.js +0 -63
- package/lib/commands.js +0 -3588
- package/lib/config-manager.js +0 -441
- package/lib/config-schema.js +0 -408
- package/lib/config-validator.js +0 -330
- package/lib/config.js +0 -122
- package/lib/errors.js +0 -305
- package/lib/incremental-sync.js +0 -274
- package/lib/marketplace.js +0 -487
- package/lib/migrations.js +0 -154
- package/lib/permission-audit.js +0 -255
- package/lib/quality-gate.js +0 -431
- package/lib/quality-rules.js +0 -373
- package/lib/utils.js +0 -150
- package/lib/version-check.js +0 -169
- package/lib/version-manifest.js +0 -171
- package/project-paradigm.md +0 -313
- package/prompts/how-to-find.md +0 -163
- package/prompts/linus-architect.md +0 -71
- package/prompts/software-architect.md +0 -173
- package/prompts/web-designer.md +0 -249
- package/scripts/fix-hooks.mjs +0 -97
- package/scripts/sync-external.mjs +0 -298
- package/scripts/sync-to-home.sh +0 -108
- package/scripts/update-registry.mjs +0 -325
- package/sources.yaml +0 -83
- package/tests/README.md +0 -263
- package/tests/commands.test.js +0 -1086
- package/tests/config-manager.test.js +0 -677
- package/tests/config-schema.test.js +0 -425
- package/tests/config-validator.test.js +0 -436
- package/tests/config.test.js +0 -100
- package/tests/errors.test.js +0 -477
- package/tests/manual/phase1-e2e.sh +0 -389
- package/tests/manual/phase2-test-cases.md +0 -311
- package/tests/manual/phase3-test-cases.md +0 -309
- package/tests/manual/phase4-test-cases.md +0 -414
- package/tests/manual/test-cases.md +0 -417
- package/tests/marketplace.test.js +0 -420
- package/tests/migrations.test.js +0 -187
- package/tests/quality-gate.test.js +0 -679
- package/tests/quality-rules.test.js +0 -619
- package/tests/sync-external.test.js +0 -214
- package/tests/update-registry.test.js +0 -251
- package/tests/utils.test.js +0 -171
- package/tests/version-check.test.js +0 -75
- package/tests/web-search.test.js +0 -392
- package/thinkinglens-silent.md +0 -138
|
@@ -1,677 +0,0 @@
|
|
|
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
|
-
});
|