diff --git a/src/utils/__tests__/claude-settings.test.ts b/src/utils/__tests__/claude-settings.test.ts index 132ad2e..27ddde7 100644 --- a/src/utils/__tests__/claude-settings.test.ts +++ b/src/utils/__tests__/claude-settings.test.ts @@ -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 = {}; - -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; - 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\"`); - }); -}); +}); \ No newline at end of file diff --git a/src/utils/__tests__/config.test.ts b/src/utils/__tests__/config.test.ts index d95f6a2..844e712 100644 --- a/src/utils/__tests__/config.test.ts +++ b/src/utils/__tests__/config.test.ts @@ -19,10 +19,9 @@ import { const MOCK_HOME_DIR = '/tmp/ccstatusline-config-test-home'; -vi.mock('os', () => ({ homedir: () => MOCK_HOME_DIR })); - let loadSettings: () => Promise; let saveSettings: (settings: Settings) => Promise; +let initConfigPath: (filePath?: string) => void; let consoleErrorSpy: ReturnType; 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 () => { diff --git a/src/utils/claude-settings.ts b/src/utils/claude-settings.ts index 8969913..76bfb30 100644 --- a/src/utils/claude-settings.ts +++ b/src/utils/claude-settings.ts @@ -168,4 +168,4 @@ export async function uninstallStatusLine(): Promise { export async function getExistingStatusLine(): Promise { const settings = await loadClaudeSettings(); return settings.statusLine?.command ?? null; -} +} \ No newline at end of file diff --git a/src/utils/colors.ts b/src/utils/colors.ts index 643f22b..7e1fa5a 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -446,4 +446,4 @@ export function getPowerlineTheme(name: string): PowerlineTheme | undefined { export function getDefaultPowerlineTheme(): string { return 'nord-aurora'; -} +} \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index 6605545..4ff139f 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -158,4 +158,4 @@ export async function saveSettings(settings: Settings): Promise { }; await writeSettingsJson(settingsWithVersion, paths); -} +} \ No newline at end of file