feat: add Accordion component for UI and Zustand store for coordinator management

- Implemented Accordion component using Radix UI for collapsible sections.
- Created Zustand store to manage coordinator execution state, command chains, logs, and interactive questions.
- Added validation tests for CLI settings type definitions, ensuring type safety and correct behavior of helper functions.
This commit is contained in:
catlog22
2026-02-03 10:02:40 +08:00
parent bcb4af3ba0
commit 5483a72e9f
82 changed files with 6156 additions and 7605 deletions

View File

@@ -0,0 +1,491 @@
/**
* CLI Settings Type Definitions Tests
*
* Test coverage:
* - ClaudeCliSettings interface type safety
* - validateSettings function with deep env validation
* - mapProviderToClaudeEnv helper function
* - createDefaultSettings helper function
*/
import { describe, it, before } from 'node:test';
import assert from 'node:assert/strict';
import {
validateSettings,
mapProviderToClaudeEnv,
createDefaultSettings,
} from '../../dist/types/cli-settings.js';
// Type for testing (interfaces are erased in JS)
type ClaudeCliSettings = {
env: Record<string, string | undefined>;
model?: string;
includeCoAuthoredBy?: boolean;
tags?: string[];
availableModels?: string[];
settingsFile?: string;
};
describe('cli-settings.ts', () => {
describe('validateSettings', () => {
describe('should validate valid ClaudeCliSettings objects', () => {
it('should accept a complete valid settings object', () => {
const validSettings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
DISABLE_AUTOUPDATER: '1',
},
model: 'sonnet',
includeCoAuthoredBy: true,
tags: ['分析', 'Debug'],
availableModels: ['opus', 'sonnet', 'haiku'],
settingsFile: '/path/to/settings.json',
};
assert.strictEqual(validateSettings(validSettings), true);
});
it('should accept settings with only required env field', () => {
const minimalSettings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
},
};
assert.strictEqual(validateSettings(minimalSettings), true);
});
it('should accept settings with empty env object', () => {
const settings = {
env: {},
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept settings with undefined optional properties', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
},
};
assert.strictEqual(validateSettings(settings), true);
});
});
describe('should reject invalid or non-object inputs', () => {
it('should reject null', () => {
assert.strictEqual(validateSettings(null), false);
});
it('should reject undefined', () => {
assert.strictEqual(validateSettings(undefined), false);
});
it('should reject number', () => {
assert.strictEqual(validateSettings(123), false);
});
it('should reject string', () => {
assert.strictEqual(validateSettings('invalid'), false);
});
it('should reject boolean', () => {
assert.strictEqual(validateSettings(true), false);
});
it('should reject array', () => {
assert.strictEqual(validateSettings([]), false);
});
});
describe('should validate env field', () => {
it('should reject missing env field', () => {
const settings = {
model: 'sonnet',
};
assert.strictEqual(validateSettings(settings), false);
});
it('should reject non-object env', () => {
const settings = {
env: 'invalid',
};
assert.strictEqual(validateSettings(settings), false);
});
it('should reject env with null value', () => {
const settings = {
env: null,
};
assert.strictEqual(validateSettings(settings), false);
});
describe('deep env validation (optimization)', () => {
it('should reject env with null value (DEEP VALIDATION)', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: null,
},
};
assert.strictEqual(validateSettings(settings), false);
});
it('should reject env with number value (DEEP VALIDATION)', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: 12345,
},
};
assert.strictEqual(validateSettings(settings), false);
});
it('should reject env with boolean value (DEEP VALIDATION)', () => {
const settings = {
env: {
DISABLE_AUTOUPDATER: true,
},
};
assert.strictEqual(validateSettings(settings), false);
});
it('should reject env with object value (DEEP VALIDATION)', () => {
const settings = {
env: {
CUSTOM: { nested: 'value' },
},
};
assert.strictEqual(validateSettings(settings), false);
});
it('should reject env with array value (DEEP VALIDATION)', () => {
const settings = {
env: {
TAGS: ['tag1', 'tag2'],
},
};
assert.strictEqual(validateSettings(settings), false);
});
it('should accept env with string values', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
DISABLE_AUTOUPDATER: '1',
},
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept env with undefined values (optional env vars)', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
ANTHROPIC_BASE_URL: undefined,
},
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept env with all undefined values', () => {
const settings = {
env: {
OPTIONAL_VAR: undefined,
},
};
assert.strictEqual(validateSettings(settings), true);
});
});
});
describe('should validate model field', () => {
it('should accept predefined model values', () => {
const settings = {
env: {},
model: 'opus',
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept custom model string', () => {
const settings = {
env: {},
model: 'custom-model-3.5',
};
assert.strictEqual(validateSettings(settings), true);
});
it('should reject non-string model', () => {
const settings = {
env: {},
model: 123,
};
assert.strictEqual(validateSettings(settings), false);
});
});
describe('should validate includeCoAuthoredBy field', () => {
it('should accept boolean includeCoAuthoredBy', () => {
const settings = {
env: {},
includeCoAuthoredBy: true,
};
assert.strictEqual(validateSettings(settings), true);
});
it('should reject non-boolean includeCoAuthoredBy', () => {
const settings = {
env: {},
includeCoAuthoredBy: 'true',
};
assert.strictEqual(validateSettings(settings), false);
});
});
describe('should validate tags field', () => {
it('should accept valid tags array', () => {
const settings = {
env: {},
tags: ['分析', 'Debug', 'testing'],
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept empty tags array', () => {
const settings = {
env: {},
tags: [],
};
assert.strictEqual(validateSettings(settings), true);
});
it('should reject non-array tags', () => {
const settings = {
env: {},
tags: 'invalid',
};
assert.strictEqual(validateSettings(settings), false);
});
});
describe('should validate availableModels field', () => {
it('should accept valid availableModels array', () => {
const settings = {
env: {},
availableModels: ['opus', 'sonnet', 'haiku', 'custom-model'],
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept empty availableModels array', () => {
const settings = {
env: {},
availableModels: [],
};
assert.strictEqual(validateSettings(settings), true);
});
it('should reject non-array availableModels', () => {
const settings = {
env: {},
availableModels: 'invalid',
};
assert.strictEqual(validateSettings(settings), false);
});
});
describe('should validate settingsFile field', () => {
it('should accept valid settingsFile string', () => {
const settings = {
env: {},
settingsFile: '/path/to/settings.json',
};
assert.strictEqual(validateSettings(settings), true);
});
it('should accept empty settingsFile string', () => {
const settings = {
env: {},
settingsFile: '',
};
assert.strictEqual(validateSettings(settings), true);
});
it('should reject non-string settingsFile', () => {
const settings = {
env: {},
settingsFile: 123,
};
assert.strictEqual(validateSettings(settings), false);
});
});
describe('edge cases and boundary conditions', () => {
it('should handle settings with many optional fields', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
DISABLE_AUTOUPDATER: '1',
CUSTOM_VAR: 'custom-value',
},
model: 'custom-model',
includeCoAuthoredBy: false,
tags: [],
availableModels: [],
settingsFile: '/path/to/settings.json',
};
assert.strictEqual(validateSettings(settings), true);
});
it('should handle settings with only env and one other field', () => {
const settings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
},
model: 'sonnet',
};
assert.strictEqual(validateSettings(settings), true);
});
});
});
describe('mapProviderToClaudeEnv', () => {
it('should map provider with apiKey only', () => {
const provider = { apiKey: 'sk-test-key' };
const env = mapProviderToClaudeEnv(provider);
assert.deepStrictEqual(env, {
ANTHROPIC_AUTH_TOKEN: 'sk-test-key',
DISABLE_AUTOUPDATER: '1',
});
});
it('should map provider with apiBase only', () => {
const provider = { apiBase: 'https://custom.api.com' };
const env = mapProviderToClaudeEnv(provider);
assert.deepStrictEqual(env, {
ANTHROPIC_BASE_URL: 'https://custom.api.com',
DISABLE_AUTOUPDATER: '1',
});
});
it('should map provider with both apiKey and apiBase', () => {
const provider = {
apiKey: 'sk-test-key',
apiBase: 'https://custom.api.com',
};
const env = mapProviderToClaudeEnv(provider);
assert.deepStrictEqual(env, {
ANTHROPIC_AUTH_TOKEN: 'sk-test-key',
ANTHROPIC_BASE_URL: 'https://custom.api.com',
DISABLE_AUTOUPDATER: '1',
});
});
it('should map empty provider to default DISABLE_AUTOUPDATER', () => {
const provider = {};
const env = mapProviderToClaudeEnv(provider);
assert.deepStrictEqual(env, {
DISABLE_AUTOUPDATER: '1',
});
});
it('should always set DISABLE_AUTOUPDATER to "1"', () => {
const env1 = mapProviderToClaudeEnv({ apiKey: 'key' });
const env2 = mapProviderToClaudeEnv({ apiBase: 'url' });
const env3 = mapProviderToClaudeEnv({});
assert.strictEqual(env1.DISABLE_AUTOUPDATER, '1');
assert.strictEqual(env2.DISABLE_AUTOUPDATER, '1');
assert.strictEqual(env3.DISABLE_AUTOUPDATER, '1');
});
});
describe('createDefaultSettings', () => {
it('should create valid default settings', () => {
const settings = createDefaultSettings();
assert.strictEqual(validateSettings(settings), true);
});
it('should include all default fields', () => {
const settings = createDefaultSettings();
assert.ok('env' in settings);
assert.ok('model' in settings);
assert.ok('includeCoAuthoredBy' in settings);
assert.ok('tags' in settings);
assert.ok('availableModels' in settings);
});
it('should have correct default values', () => {
const settings = createDefaultSettings();
assert.deepStrictEqual(settings.env, {
DISABLE_AUTOUPDATER: '1',
});
assert.strictEqual(settings.model, 'sonnet');
assert.strictEqual(settings.includeCoAuthoredBy, false);
assert.deepStrictEqual(settings.tags, []);
assert.deepStrictEqual(settings.availableModels, []);
});
});
describe('TypeScript type safety', () => {
it('should enforce ClaudeCliSettings interface structure', () => {
// This test verifies TypeScript compilation catches type errors
const validSettings = {
env: {
ANTHROPIC_AUTH_TOKEN: 'sk-ant-123',
},
model: 'opus',
includeCoAuthoredBy: true,
tags: ['tag1'],
availableModels: ['model1'],
settingsFile: '/path/to/file',
};
// Type assertion: all fields should be present and of correct type
assert.strictEqual(typeof validSettings.env, 'object');
assert.strictEqual(typeof validSettings.model, 'string');
assert.strictEqual(typeof validSettings.includeCoAuthoredBy, 'boolean');
assert.strictEqual(Array.isArray(validSettings.tags), true);
assert.strictEqual(Array.isArray(validSettings.availableModels), true);
assert.strictEqual(typeof validSettings.settingsFile, 'string');
});
});
});