test: isolate config-path behavior across suite

This commit is contained in:
Matthew Breedlove
2026-03-04 09:59:30 -05:00
parent 9c0f5feab6
commit 5c0d211ddf
5 changed files with 49 additions and 101 deletions
+41 -96
View File
@@ -1,42 +1,44 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
afterEach,
afterAll,
beforeAll,
afterEach,
beforeEach,
describe,
expect,
it,
vi,
type Mock
it
} from 'vitest';
import {
getConfigPath,
isCustomConfigPath
} from '../config';
CCSTATUSLINE_COMMANDS,
getClaudeSettingsPath,
installStatusLine,
isKnownCommand
} from '../claude-settings';
import { initConfigPath } from '../config';
vi.mock('../config', () => ({
getConfigPath: vi.fn(() => '/default/settings.json'),
isCustomConfigPath: vi.fn(() => false)
}));
const ORIGINAL_CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR;
let testClaudeConfigDir = '';
const mockIsCustomConfigPath = isCustomConfigPath as Mock;
const mockGetConfigPath = getConfigPath as Mock;
const ORIGINAL_PLATFORM = process.platform;
function setProcessPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', {
value: platform,
configurable: true
});
function readInstalledCommand(): string {
const settingsPath = getClaudeSettingsPath();
const content = fs.readFileSync(settingsPath, 'utf-8');
const data = JSON.parse(content) as { statusLine?: { command?: string } };
return data.statusLine?.command ?? '';
}
// Safety net: point CLAUDE_CONFIG_DIR at a temp path so even if the fs mock
// leaks, writes will never land in the user's real ~/.claude directory.
const ORIGINAL_CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR;
beforeEach(() => {
testClaudeConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-claude-settings-'));
process.env.CLAUDE_CONFIG_DIR = testClaudeConfigDir;
initConfigPath();
});
beforeAll(() => {
process.env.CLAUDE_CONFIG_DIR = '/tmp/ccstatusline-test-fake-claude-config';
afterEach(() => {
initConfigPath();
if (testClaudeConfigDir) {
fs.rmSync(testClaudeConfigDir, { recursive: true, force: true });
}
});
afterAll(() => {
@@ -47,25 +49,6 @@ afterAll(() => {
}
});
// Capture settings written by installStatusLine via the mock writeFile callback.
let savedSettings: Record<string, unknown> = {};
vi.mock('fs', () => ({
existsSync: vi.fn(() => false),
statSync: vi.fn(),
promises: {
readFile: vi.fn(() => Promise.resolve('{}')),
writeFile: vi.fn((_path: string, content: string) => {
savedSettings = JSON.parse(content) as Record<string, unknown>;
return Promise.resolve(undefined);
}),
mkdir: vi.fn(() => Promise.resolve(undefined))
}
}));
// Dynamic import so the module picks up the mocked fs references.
const { CCSTATUSLINE_COMMANDS, isKnownCommand, installStatusLine } = await import('../claude-settings');
describe('isKnownCommand', () => {
it('should match exact NPM command', () => {
expect(isKnownCommand(CCSTATUSLINE_COMMANDS.NPM)).toBe(true);
@@ -113,77 +96,39 @@ describe('isKnownCommand', () => {
});
describe('buildCommand via installStatusLine', () => {
beforeEach(() => {
savedSettings = {};
setProcessPlatform(ORIGINAL_PLATFORM);
mockIsCustomConfigPath.mockReturnValue(false);
mockGetConfigPath.mockReturnValue('/default/settings.json');
});
afterEach(() => {
setProcessPlatform(ORIGINAL_PLATFORM);
});
it('should use base command when no custom config path', async () => {
mockIsCustomConfigPath.mockReturnValue(false);
initConfigPath();
await installStatusLine(false);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(CCSTATUSLINE_COMMANDS.NPM);
expect(readInstalledCommand()).toBe(CCSTATUSLINE_COMMANDS.NPM);
});
it('should append --config with simple path (no quoting needed)', async () => {
mockIsCustomConfigPath.mockReturnValue(true);
mockGetConfigPath.mockReturnValue('/tmp/settings.json');
initConfigPath('/tmp/settings.json');
await installStatusLine(false);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config /tmp/settings.json`);
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config /tmp/settings.json`);
});
it('should quote path with spaces', async () => {
mockIsCustomConfigPath.mockReturnValue(true);
mockGetConfigPath.mockReturnValue('/my path/settings.json');
initConfigPath('/my path/settings.json');
await installStatusLine(false);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config '/my path/settings.json'`);
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config '/my path/settings.json'`);
});
it('should quote path with parentheses', async () => {
mockIsCustomConfigPath.mockReturnValue(true);
mockGetConfigPath.mockReturnValue('/my(path)/settings.json');
initConfigPath('/my(path)/settings.json');
await installStatusLine(false);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config '/my(path)/settings.json'`);
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config '/my(path)/settings.json'`);
});
it('should escape embedded single quotes in path', async () => {
mockIsCustomConfigPath.mockReturnValue(true);
mockGetConfigPath.mockReturnValue('/my\'path/settings.json');
initConfigPath('/my\'path/settings.json');
await installStatusLine(false);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config '/my'\\''path/settings.json'`);
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config '/my'\\''path/settings.json'`);
});
it('should use bunx command when useBunx is true', async () => {
mockIsCustomConfigPath.mockReturnValue(true);
mockGetConfigPath.mockReturnValue('/my path/settings.json');
initConfigPath('/my path/settings.json');
await installStatusLine(true);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(`${CCSTATUSLINE_COMMANDS.BUNX} --config '/my path/settings.json'`);
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.BUNX} --config '/my path/settings.json'`);
});
it('should use Windows-safe double quoting for custom config paths', async () => {
setProcessPlatform('win32');
mockIsCustomConfigPath.mockReturnValue(true);
mockGetConfigPath.mockReturnValue('C:\\Users\\Alice\\My Settings\\settings.json');
await installStatusLine(false);
const statusLine = savedSettings.statusLine as { command: string };
expect(statusLine.command).toBe(`${CCSTATUSLINE_COMMANDS.NPM} --config \"C:\\Users\\Alice\\My Settings\\settings.json\"`);
});
});
});
+5 -2
View File
@@ -19,10 +19,9 @@ import {
const MOCK_HOME_DIR = '/tmp/ccstatusline-config-test-home';
vi.mock('os', () => ({ homedir: () => MOCK_HOME_DIR }));
let loadSettings: () => Promise<Settings>;
let saveSettings: (settings: Settings) => Promise<void>;
let initConfigPath: (filePath?: string) => void;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
function getSettingsPaths(): { configDir: string; settingsPath: string; backupPath: string } {
@@ -39,10 +38,13 @@ describe('config utilities', () => {
const configModule = await import('../config');
loadSettings = configModule.loadSettings;
saveSettings = configModule.saveSettings;
initConfigPath = configModule.initConfigPath;
});
beforeEach(() => {
fs.rmSync(MOCK_HOME_DIR, { recursive: true, force: true });
const { settingsPath } = getSettingsPaths();
initConfigPath(settingsPath);
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
});
@@ -52,6 +54,7 @@ describe('config utilities', () => {
afterAll(() => {
fs.rmSync(MOCK_HOME_DIR, { recursive: true, force: true });
initConfigPath();
});
it('writes defaults when settings file does not exist', async () => {
+1 -1
View File
@@ -168,4 +168,4 @@ export async function uninstallStatusLine(): Promise<void> {
export async function getExistingStatusLine(): Promise<string | null> {
const settings = await loadClaudeSettings();
return settings.statusLine?.command ?? null;
}
}
+1 -1
View File
@@ -446,4 +446,4 @@ export function getPowerlineTheme(name: string): PowerlineTheme | undefined {
export function getDefaultPowerlineTheme(): string {
return 'nord-aurora';
}
}
+1 -1
View File
@@ -158,4 +158,4 @@ export async function saveSettings(settings: Settings): Promise<void> {
};
await writeSettingsJson(settingsWithVersion, paths);
}
}