Files
wehub-resource-sync 719032b19f
Update Schema / Update configuration json schema (push) Has been cancelled
Memory Benchmark / Memory Test (Full Analysis) (push) Has been cancelled
CI Quality / Lint GitHub Actions (actionlint) (push) Has been cancelled
CI Quality / Lint GitHub Actions (zizmor) (push) Has been cancelled
CI Quality / Check typos (push) Has been cancelled
CI / Test coverage (push) Has been cancelled
CI / Test (22.x, windows-latest) (push) Has been cancelled
CI / Test (24.x, macos-latest) (push) Has been cancelled
CI / Test (24.x, ubuntu-latest) (push) Has been cancelled
CI / Test (24.x, windows-latest) (push) Has been cancelled
CI / Test (26.x, macos-latest) (push) Has been cancelled
CI / Test (26.x, ubuntu-latest) (push) Has been cancelled
CI / Test (26.x, windows-latest) (push) Has been cancelled
CI / Test with Bun (latest, macos-latest) (push) Has been cancelled
CI / Test with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Test with Bun (latest, windows-latest) (push) Has been cancelled
CI / Build and run (22.x, macos-latest) (push) Has been cancelled
CI / Build and run (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (22.x, windows-latest) (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CI Browser Extension / Lint Browser Extension (push) Has been cancelled
CI Browser Extension / Test Browser Extension (push) Has been cancelled
Memory Benchmark / Memory Test (push) Has been cancelled
CI Website / Lint Website Client (push) Has been cancelled
CI Website / Lint Website Server (push) Has been cancelled
CI Website / Test Website Server (push) Has been cancelled
CI Website / Bundle Website Server (push) Has been cancelled
CI / Lint Biome (push) Has been cancelled
CI / Lint oxlint (push) Has been cancelled
CI / Lint TypeScript (push) Has been cancelled
CI / Lint Secretlint (push) Has been cancelled
CI / Test (22.x, macos-latest) (push) Has been cancelled
CI / Test (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, macos-latest) (push) Has been cancelled
CI / Build and run (24.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, windows-latest) (push) Has been cancelled
CI / Build and run (26.x, macos-latest) (push) Has been cancelled
CI / Build and run (26.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (26.x, windows-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, macos-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Docker / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker / build (linux/arm/v7, ubuntu-24.04-arm) (push) Has been cancelled
Docker / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker / merge (push) Has been cancelled
Pack repository with Repomix / pack-repo (push) Has been cancelled
Performance Benchmark History / Benchmark (macos-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (ubuntu-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (windows-latest) (push) Has been cancelled
Performance Benchmark History / Store Results (push) Has been cancelled
Test Repomix Action / Test Node.js 22 (push) Has been cancelled
Test Repomix Action / Test Node.js 24 (push) Has been cancelled
Test Repomix Action / Test Node.js 26 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:37 +08:00

501 lines
23 KiB
TypeScript

import type { Stats } from 'node:fs';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { loadFileConfig, mergeConfigs } from '../../src/config/configLoad.js';
import { defaultConfig, type RepomixConfigCli, type RepomixConfigFile } from '../../src/config/configSchema.js';
import { getGlobalDirectory } from '../../src/config/globalDirectory.js';
import { RepomixConfigValidationError } from '../../src/shared/errorHandle.js';
import { logger } from '../../src/shared/logger.js';
vi.mock('node:fs/promises');
vi.mock('../../src/shared/logger', () => ({
logger: {
trace: vi.fn(),
note: vi.fn(),
log: vi.fn(),
warn: vi.fn(),
},
}));
vi.mock('../../src/config/globalDirectory', () => ({
getGlobalDirectory: vi.fn(),
}));
describe('configLoad', () => {
beforeEach(() => {
vi.resetAllMocks();
process.env = {};
});
describe('loadFileConfig', () => {
test('should load and parse a valid local config file', async () => {
const mockConfig = {
output: { filePath: 'test-output.txt' },
ignore: { useDefaultPatterns: true },
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
const result = await loadFileConfig(process.cwd(), 'test-config.json');
expect(result).toEqual(mockConfig);
});
test('should throw RepomixConfigValidationError for invalid config', async () => {
const invalidConfig = {
output: { filePath: 123, style: 'invalid' }, // Invalid filePath type and invalid style
ignore: { useDefaultPatterns: 'not a boolean' }, // Invalid type
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(invalidConfig));
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
await expect(loadFileConfig(process.cwd(), 'test-config.json')).rejects.toThrow(RepomixConfigValidationError);
});
test('should load global config when local config is not found', async () => {
const mockGlobalConfig = {
output: { filePath: 'global-output.txt' },
ignore: { useDefaultPatterns: false },
};
vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix');
vi.mocked(fs.stat)
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.cjs
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json5
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.jsonc
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.cjs
.mockResolvedValueOnce({ isFile: () => true } as Stats); // Global repomix.config.json5
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockGlobalConfig));
const result = await loadFileConfig(process.cwd(), null);
expect(result).toEqual(mockGlobalConfig);
expect(fs.readFile).toHaveBeenCalledWith(path.join('/global/repomix', 'repomix.config.json5'), 'utf-8');
});
test('should return an empty object if no config file is found', async () => {
const loggerSpy = vi.spyOn(logger, 'log').mockImplementation(vi.fn());
vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix');
vi.mocked(fs.stat).mockRejectedValue(new Error('File not found'));
const result = await loadFileConfig(process.cwd(), null);
expect(result).toEqual({});
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('No custom config found'));
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.json5'));
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.jsonc'));
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.json'));
});
test('should throw an error for invalid JSON', async () => {
vi.mocked(fs.readFile).mockResolvedValue('invalid json');
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
await expect(loadFileConfig(process.cwd(), 'test-config.json')).rejects.toThrow('Invalid syntax');
});
test('should parse config file with comments', async () => {
const configWithComments = `{
// Output configuration
"output": {
"filePath": "test-output.txt"
},
/* Ignore configuration */
"ignore": {
"useGitignore": true // Use .gitignore file
}
}`;
vi.mocked(fs.readFile).mockResolvedValue(configWithComments);
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
const result = await loadFileConfig(process.cwd(), 'test-config.json');
expect(result).toEqual({
output: { filePath: 'test-output.txt' },
ignore: { useGitignore: true },
});
});
test('should parse config file with JSON5 features', async () => {
const configWithJSON5Features = `{
// Output configuration
output: {
filePath: 'test-output.txt',
style: 'plain',
},
/* Ignore configuration */
ignore: {
useGitignore: true, // Use .gitignore file
customPatterns: [
'*.log',
'*.tmp',
'*.temp', // Trailing comma
],
},
}`;
vi.mocked(fs.readFile).mockResolvedValue(configWithJSON5Features);
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
const result = await loadFileConfig(process.cwd(), 'test-config.json');
expect(result).toEqual({
output: { filePath: 'test-output.txt', style: 'plain' },
ignore: {
useGitignore: true,
customPatterns: ['*.log', '*.tmp', '*.temp'],
},
});
});
test('should load .jsonc config file with priority order', async () => {
const mockConfig = {
output: { filePath: 'jsonc-output.txt' },
ignore: { useDefaultPatterns: true },
};
vi.mocked(fs.stat)
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.cjs
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.json5
.mockResolvedValueOnce({ isFile: () => true } as Stats); // repomix.config.jsonc
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const result = await loadFileConfig(process.cwd(), null);
expect(result).toEqual(mockConfig);
expect(fs.readFile).toHaveBeenCalledWith(path.resolve(process.cwd(), 'repomix.config.jsonc'), 'utf-8');
});
test('should prioritize .json5 over .jsonc and .json', async () => {
const mockConfig = {
output: { filePath: 'json5-output.txt' },
ignore: { useDefaultPatterns: true },
};
vi.mocked(fs.stat)
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.cjs
.mockResolvedValueOnce({ isFile: () => true } as Stats); // repomix.config.json5 exists
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const result = await loadFileConfig(process.cwd(), null);
expect(result).toEqual(mockConfig);
expect(fs.readFile).toHaveBeenCalledWith(path.resolve(process.cwd(), 'repomix.config.json5'), 'utf-8');
// Should not check for .jsonc or .json since .json5 was found
expect(fs.stat).toHaveBeenCalledTimes(7);
});
test('should throw RepomixError when specific config file does not exist', async () => {
const nonExistentConfigPath = 'non-existent-config.json';
vi.mocked(fs.stat).mockRejectedValue(new Error('File not found'));
await expect(loadFileConfig(process.cwd(), nonExistentConfigPath)).rejects.toThrow(
`Config file not found at ${nonExistentConfigPath}`,
);
});
test('should throw RepomixError for unsupported config file format', async () => {
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
await expect(loadFileConfig(process.cwd(), 'test-config.yaml')).rejects.toThrow('Unsupported config file format');
});
test('should throw RepomixError for config file with unsupported extension', async () => {
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
await expect(loadFileConfig(process.cwd(), 'test-config.toml')).rejects.toThrow('Unsupported config file format');
});
test('should handle general errors when loading config', async () => {
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));
await expect(loadFileConfig(process.cwd(), 'test-config.json')).rejects.toThrow('Error loading config');
});
test('should handle non-Error objects when loading config', async () => {
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
vi.mocked(fs.readFile).mockRejectedValue('String error');
await expect(loadFileConfig(process.cwd(), 'test-config.json')).rejects.toThrow('Error loading config');
});
test('should skip local config auto-detection when skipLocalConfig is true', async () => {
vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix');
// All local and global config files not found
vi.mocked(fs.stat).mockRejectedValue(new Error('File not found'));
const result = await loadFileConfig('/project/repo', null, { skipLocalConfig: true });
expect(result).toEqual({});
});
test('should still load global config when skipLocalConfig is true', async () => {
const mockGlobalConfig = { output: { style: 'markdown' } };
vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix');
vi.mocked(fs.stat)
// Local config search (for skip-log detection) — all not found
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.cjs
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json5
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.jsonc
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json
// Global config search
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.cjs
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.json5
.mockRejectedValueOnce(new Error('File not found')) // Global repomix.config.jsonc
.mockResolvedValueOnce({ isFile: () => true } as Stats); // Global repomix.config.json
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockGlobalConfig));
const result = await loadFileConfig('/project/repo', null, { skipLocalConfig: true });
expect(result).toEqual(mockGlobalConfig);
});
test('should log a message when skipping config in remote mode', async () => {
vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix');
// Local config exists but should be skipped
vi.mocked(fs.stat)
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.ts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.mts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.cts
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.js
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.mjs
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.cjs
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.json5
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.jsonc
.mockResolvedValueOnce({ isFile: () => true } as Stats) // repomix.config.json — found
.mockRejectedValue(new Error('File not found')); // global configs
await loadFileConfig('/tmp/repomix-clone', null, { skipLocalConfig: true });
expect(logger.note).toHaveBeenCalledWith(expect.stringContaining('Skipping config file'));
expect(logger.note).toHaveBeenCalledWith(expect.stringContaining('--remote-trust-config'));
});
test('should still respect --config flag even when skipLocalConfig is true', async () => {
const mockConfig = { output: { filePath: 'custom-output.xml' } };
vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true } as Stats);
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const result = await loadFileConfig('/tmp/repomix-clone', '/home/user/my-config.json', {
skipLocalConfig: true,
});
expect(result).toEqual(mockConfig);
});
});
describe('mergeConfigs', () => {
test('should correctly merge configs', () => {
const fileConfig: RepomixConfigFile = {
output: { filePath: 'file-output.txt' },
ignore: { useDefaultPatterns: true, customPatterns: ['file-ignore'] },
};
const cliConfig: RepomixConfigCli = {
output: { filePath: 'cli-output.txt' },
ignore: { customPatterns: ['cli-ignore'] },
};
const result = mergeConfigs(process.cwd(), fileConfig, cliConfig);
expect(result.output.filePath).toBe('cli-output.txt');
expect(result.ignore.useDefaultPatterns).toBe(true);
expect(result.ignore.customPatterns).toContain('file-ignore');
expect(result.ignore.customPatterns).toContain('cli-ignore');
});
test('should throw RepomixConfigValidationError for invalid merged config', () => {
const fileConfig: RepomixConfigFile = {
output: { filePath: 'file-output.txt', style: 'plain' },
};
const cliConfig: RepomixConfigCli = {
// @ts-expect-error
output: { style: 'invalid' }, // Invalid style
};
expect(() => mergeConfigs(process.cwd(), fileConfig, cliConfig)).toThrow(RepomixConfigValidationError);
});
test('should merge nested git config correctly', () => {
const fileConfig: RepomixConfigFile = {
output: { git: { sortByChanges: false } },
};
const cliConfig: RepomixConfigCli = {
output: { git: { includeDiffs: true } },
};
const merged = mergeConfigs(process.cwd(), fileConfig, cliConfig);
// Both configs should be applied
expect(merged.output.git.sortByChanges).toBe(false);
expect(merged.output.git.includeDiffs).toBe(true);
// Defaults should still be present
expect(merged.output.git.sortByChangesMaxCommits).toBe(100);
});
test('should default output filePathStyle to target-relative', () => {
const merged = mergeConfigs(process.cwd(), {}, {});
expect(merged.output.filePathStyle).toBe('target-relative');
});
test('should merge output filePathStyle from file config', () => {
const merged = mergeConfigs(process.cwd(), { output: { filePathStyle: 'cwd-relative' } }, {});
expect(merged.output.filePathStyle).toBe('cwd-relative');
});
test('should let CLI output filePathStyle override file config', () => {
const merged = mergeConfigs(
process.cwd(),
{ output: { filePathStyle: 'target-relative' } },
{ output: { filePathStyle: 'cwd-relative' } },
);
expect(merged.output.filePathStyle).toBe('cwd-relative');
});
test('should preserve output.patterns from the file config through the merge', () => {
// Regression guard: output.patterns is declared only on the base/file schema
// member of repomixConfigMergedSchema's intersect (the default member's output
// does not declare it). If the intersect ever stopped merging per-schema
// outputs, config-file inclusion-level patterns would be silently dropped.
const fileConfig: RepomixConfigFile = {
output: {
patterns: [
{ pattern: 'docs/**/*', compress: true },
{ pattern: 'website/**/*', directoryStructureOnly: true },
],
},
};
const merged = mergeConfigs(process.cwd(), fileConfig, {});
expect(merged.output.patterns).toEqual([
{ pattern: 'docs/**/*', compress: true },
{ pattern: 'website/**/*', directoryStructureOnly: true },
]);
});
test('should preserve input.processors from the file config', () => {
const fileConfig: RepomixConfigFile = {
input: {
processors: [{ pattern: '**/*.json', command: 'toon {file}', timeout: 30000, onError: 'skip' }],
},
};
const merged = mergeConfigs(process.cwd(), fileConfig, {});
expect(merged.input.processors).toEqual([
{ pattern: '**/*.json', command: 'toon {file}', timeout: 30000, onError: 'skip' },
]);
});
test('should apply enableFileProcessors from the CLI config', () => {
const merged = mergeConfigs(process.cwd(), {}, { enableFileProcessors: true });
expect(merged.enableFileProcessors).toBe(true);
});
test('should ignore enableFileProcessors coming from the file config (gate is CLI-only)', () => {
// A malicious repo config must not be able to self-authorize command execution.
// mergeConfigs only reads the gate from the CLI config, never the file config.
const fileConfig = {
enableFileProcessors: true,
input: { processors: [{ pattern: '**/*', command: 'evil {file}' }] },
} as unknown as RepomixConfigFile;
const merged = mergeConfigs(process.cwd(), fileConfig, {});
expect(merged.enableFileProcessors).toBeUndefined();
});
test('should not mutate defaultConfig', () => {
const originalFilePath = defaultConfig.output.filePath;
const fileConfig: RepomixConfigFile = {
output: { style: 'markdown' },
};
mergeConfigs(process.cwd(), fileConfig, {});
// defaultConfig should remain unchanged
expect(defaultConfig.output.filePath).toBe(originalFilePath);
});
test('should merge tokenCount config correctly', () => {
const fileConfig: RepomixConfigFile = {
tokenCount: { encoding: 'cl100k_base' },
};
const merged = mergeConfigs(process.cwd(), fileConfig, {});
expect(merged.tokenCount.encoding).toBe('cl100k_base');
});
test('should map default filename to style when only style is provided via CLI', () => {
const merged = mergeConfigs(process.cwd(), {}, { output: { style: 'markdown' } });
expect(merged.output.filePath).toBe('repomix-output.md');
expect(merged.output.style).toBe('markdown');
});
test('should keep explicit CLI output filePath even when style is provided', () => {
const merged = mergeConfigs(process.cwd(), {}, { output: { style: 'markdown', filePath: 'custom-output.any' } });
expect(merged.output.filePath).toBe('custom-output.any');
expect(merged.output.style).toBe('markdown');
});
test('should keep explicit file config filePath even when style is provided via CLI', () => {
const merged = mergeConfigs(
process.cwd(),
{ output: { filePath: 'from-file.txt' } },
{ output: { style: 'markdown' } },
);
expect(merged.output.filePath).toBe('from-file.txt');
expect(merged.output.style).toBe('markdown');
});
test('should map default filename when style provided in file config and no filePath anywhere', () => {
const merged = mergeConfigs(process.cwd(), { output: { style: 'plain' } }, {});
expect(merged.output.filePath).toBe('repomix-output.txt');
expect(merged.output.style).toBe('plain');
});
test('should merge skillGenerate boolean from CLI config', () => {
const merged = mergeConfigs(process.cwd(), {}, { skillGenerate: true });
expect(merged.skillGenerate).toBe(true);
});
test('should merge skillGenerate string from CLI config', () => {
const merged = mergeConfigs(process.cwd(), {}, { skillGenerate: 'my-custom-skill' });
expect(merged.skillGenerate).toBe('my-custom-skill');
});
test('should not include skillGenerate in merged config when undefined', () => {
const merged = mergeConfigs(process.cwd(), {}, {});
expect(merged.skillGenerate).toBeUndefined();
});
test('should not allow skillGenerate from file config (CLI-only option)', () => {
// File config should not have skillGenerate - it's CLI-only
// This test verifies that even if somehow passed, file config doesn't affect it
const merged = mergeConfigs(process.cwd(), {}, { skillGenerate: 'from-cli' });
expect(merged.skillGenerate).toBe('from-cli');
});
});
});