feat: build CLI core framework with Commander.js
Add CLI entry point with Commander.js, config management (~/.mcpctl/config.json with Zod validation), output formatters (table/json/yaml), config and status commands with dependency injection for testing. Fix sanitizeString regex ordering. 67 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
83
src/cli/tests/config/loader.test.ts
Normal file
83
src/cli/tests/config/loader.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { loadConfig, saveConfig, mergeConfig, getConfigPath, DEFAULT_CONFIG } from '../../src/config/index.js';
|
||||
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'mcpctl-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('getConfigPath', () => {
|
||||
it('returns path within config dir', () => {
|
||||
const path = getConfigPath('/tmp/mcpctl');
|
||||
expect(path).toBe('/tmp/mcpctl/config.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('returns defaults when no config file exists', () => {
|
||||
const config = loadConfig({ configDir: tempDir });
|
||||
expect(config).toEqual(DEFAULT_CONFIG);
|
||||
});
|
||||
|
||||
it('loads config from file', () => {
|
||||
saveConfig({ ...DEFAULT_CONFIG, daemonUrl: 'http://custom:5000' }, { configDir: tempDir });
|
||||
const config = loadConfig({ configDir: tempDir });
|
||||
expect(config.daemonUrl).toBe('http://custom:5000');
|
||||
});
|
||||
|
||||
it('applies defaults for missing fields', () => {
|
||||
const { writeFileSync } = require('node:fs') as typeof import('node:fs');
|
||||
writeFileSync(join(tempDir, 'config.json'), '{"daemonUrl":"http://x:1"}');
|
||||
const config = loadConfig({ configDir: tempDir });
|
||||
expect(config.daemonUrl).toBe('http://x:1');
|
||||
expect(config.registries).toEqual(['official', 'glama', 'smithery']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveConfig', () => {
|
||||
it('creates config file', () => {
|
||||
saveConfig(DEFAULT_CONFIG, { configDir: tempDir });
|
||||
expect(existsSync(join(tempDir, 'config.json'))).toBe(true);
|
||||
});
|
||||
|
||||
it('creates config directory if missing', () => {
|
||||
const nested = join(tempDir, 'nested', 'dir');
|
||||
saveConfig(DEFAULT_CONFIG, { configDir: nested });
|
||||
expect(existsSync(join(nested, 'config.json'))).toBe(true);
|
||||
});
|
||||
|
||||
it('round-trips configuration', () => {
|
||||
const custom = {
|
||||
...DEFAULT_CONFIG,
|
||||
daemonUrl: 'http://custom:9000',
|
||||
registries: ['official' as const],
|
||||
outputFormat: 'json' as const,
|
||||
};
|
||||
saveConfig(custom, { configDir: tempDir });
|
||||
const loaded = loadConfig({ configDir: tempDir });
|
||||
expect(loaded).toEqual(custom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeConfig', () => {
|
||||
it('merges overrides into existing config', () => {
|
||||
saveConfig(DEFAULT_CONFIG, { configDir: tempDir });
|
||||
const merged = mergeConfig({ daemonUrl: 'http://new:1234' }, { configDir: tempDir });
|
||||
expect(merged.daemonUrl).toBe('http://new:1234');
|
||||
expect(merged.registries).toEqual(DEFAULT_CONFIG.registries);
|
||||
});
|
||||
|
||||
it('works when no config file exists', () => {
|
||||
const merged = mergeConfig({ outputFormat: 'yaml' }, { configDir: tempDir });
|
||||
expect(merged.outputFormat).toBe('yaml');
|
||||
expect(merged.daemonUrl).toBe('http://localhost:3000');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user