fix(cli): reset themeManager between tests to ensure isolation (#20598)

This commit is contained in:
N. Taylor Mullen
2026-02-28 11:45:31 -08:00
committed by GitHub
parent b2214a6676
commit cd3a8c3f07
4 changed files with 54 additions and 8 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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];

View File

@@ -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';