diff --git a/packages/cli/src/ui/themes/theme-manager.test.ts b/packages/cli/src/ui/themes/theme-manager.test.ts index 40f55ec860..a655530b3b 100644 --- a/packages/cli/src/ui/themes/theme-manager.test.ts +++ b/packages/cli/src/ui/themes/theme-manager.test.ts @@ -56,7 +56,8 @@ const validCustomTheme: CustomTheme = { describe('ThemeManager', () => { beforeEach(() => { - // Reset themeManager state + // Reset themeManager state and inject mocks + themeManager.reinitialize({ fs, homedir: os.homedir }); themeManager.loadCustomThemes({}); themeManager.setActiveTheme(DEFAULT_THEME.name); themeManager.setTerminalBackground(undefined); diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 307666749b..da54ba5d3e 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -61,7 +61,13 @@ class ThemeManager { private cachedSemanticColors: SemanticColors | undefined; private lastCacheKey: string | undefined; - constructor() { + private fs: typeof fs; + private homedir: () => string; + + constructor(dependencies?: { fs?: typeof fs; homedir?: () => string }) { + this.fs = dependencies?.fs ?? fs; + this.homedir = dependencies?.homedir ?? homedir; + this.availableThemes = [ AyuDark, AyuLight, @@ -242,10 +248,44 @@ class ThemeManager { } /** - * Sets the active theme. - * @param themeName The name of the theme to set as active. - * @returns True if the theme was successfully set, false otherwise. + * Clears all themes loaded from files. + * This is primarily for testing purposes to reset state between tests. */ + clearFileThemes(): void { + this.fileThemes.clear(); + } + + /** + * Re-initializes the ThemeManager with new dependencies. + * This is primarily for testing to allow injecting mocks. + */ + reinitialize(dependencies: { fs?: typeof fs; homedir?: () => string }): void { + if (dependencies.fs) { + this.fs = dependencies.fs; + } + if (dependencies.homedir) { + this.homedir = dependencies.homedir; + } + } + + /** + * Resets the ThemeManager state to defaults. + * This is for testing purposes to ensure test isolation. + */ + resetForTesting(dependencies?: { + fs?: typeof fs; + homedir?: () => string; + }): void { + if (dependencies) { + this.reinitialize(dependencies); + } + this.settingsThemes.clear(); + this.extensionThemes.clear(); + this.fileThemes.clear(); + this.activeTheme = DEFAULT_THEME; + this.terminalBackground = undefined; + this.clearCache(); + } setActiveTheme(themeName: string | undefined): boolean { const theme = this.findThemeByName(themeName); if (!theme) { @@ -505,7 +545,7 @@ class ThemeManager { private loadThemeFromFile(themePath: string): Theme | undefined { try { // realpathSync resolves the path and throws if it doesn't exist. - const canonicalPath = fs.realpathSync(path.resolve(themePath)); + const canonicalPath = this.fs.realpathSync(path.resolve(themePath)); // 1. Check cache using the canonical path. if (this.fileThemes.has(canonicalPath)) { @@ -513,7 +553,7 @@ class ThemeManager { } // 2. Perform security check. - const homeDir = path.resolve(homedir()); + const homeDir = path.resolve(this.homedir()); if (!canonicalPath.startsWith(homeDir)) { debugLogger.warn( `Theme file at "${themePath}" is outside your home directory. ` + @@ -523,7 +563,7 @@ class ThemeManager { } // 3. Read, parse, and validate the theme file. - const themeContent = fs.readFileSync(canonicalPath, 'utf-8'); + const themeContent = this.fs.readFileSync(canonicalPath, 'utf-8'); // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const customThemeConfig = JSON.parse(themeContent) as CustomTheme; diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts index dc75dd217b..aee1c1345e 100644 --- a/packages/cli/test-setup.ts +++ b/packages/cli/test-setup.ts @@ -7,6 +7,7 @@ import { vi, beforeEach, afterEach } from 'vitest'; import { format } from 'node:util'; import { coreEvents } from '@google/gemini-cli-core'; +import { themeManager } from './src/ui/themes/theme-manager.js'; // Unset CI environment variable so that ink renders dynamically as it does in a real terminal if (process.env.CI !== undefined) { @@ -32,6 +33,9 @@ let consoleErrorSpy: vi.SpyInstance; let actWarnings: Array<{ message: string; stack: string }> = []; beforeEach(() => { + // Reset themeManager state to ensure test isolation + themeManager.resetForTesting(); + actWarnings = []; consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => { const firstArg = args[0]; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be7db0a65d..8ce5e77d81 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -188,6 +188,7 @@ export { OAuthUtils } from './mcp/oauth-utils.js'; export * from './telemetry/index.js'; export * from './telemetry/billingEvents.js'; export { logBillingEvent } from './telemetry/loggers.js'; +export * from './telemetry/constants.js'; export { sessionId, createSessionId } from './utils/session.js'; export * from './utils/compatibility.js'; export * from './utils/browser.js';