test: stabilize CI by isolating cross-file mocks

Convert module-level mocks to scoped spies and restore global stubs between tests to prevent Bun/Vitest cross-file leakage and flaky Actions failures.
This commit is contained in:
Matthew Breedlove committed 2026-03-04 11:26:34 -05:00
1 parent 78c7bc16f0
commit 59d43589c9
13 files changed
+174 -151

No files matched your search

+11 -6
View File
@@ -1,5 +1,6 @@
import { execSync } from 'child_process';
import {
afterEach,
beforeEach,
describe,
expect,
@@ -14,18 +15,22 @@ import {
vi.mock('child_process', () => ({ execSync: vi.fn() }));
const mockExecSync = execSync as unknown as {
mock: { calls: unknown[][] };
mockImplementationOnce: (impl: () => never) => void;
mockReturnValueOnce: (value: string) => void;
};
describe('terminal utils', () => {
const mockExecSync = execSync as unknown as {
mock: { calls: unknown[][] };
mockImplementationOnce: (impl: () => never) => void;
mockReturnValueOnce: (value: string) => void;
};
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('returns width from tty probe when available', () => {
mockExecSync.mockReturnValueOnce('ttys001\n');
mockExecSync.mockReturnValueOnce('120\n');
+18 -12
View File
@@ -1,4 +1,6 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
@@ -6,25 +8,32 @@ import {
} from 'vitest';
import type { WidgetItem } from '../../types/Widget';
import { fetchUsageData } from '../usage';
import * as usage from '../usage';
import {
hasUsageDependentWidgets,
prefetchUsageDataIfNeeded
} from '../usage-prefetch';
vi.mock('../usage', () => ({ fetchUsageData: vi.fn() }));
const mockFetchUsageData = fetchUsageData as unknown as {
mockClear: () => void;
mockResolvedValue: (value: unknown) => void;
mock: { calls: unknown[][] };
};
import type { UsageData } from '../usage-types';
function makeLines(...lineItems: WidgetItem[][]): WidgetItem[][] {
return lineItems;
}
describe('usage prefetch', () => {
let mockFetchUsageData: {
mock: { calls: unknown[][] };
mockResolvedValue: (value: UsageData) => void;
};
beforeEach(() => {
vi.restoreAllMocks();
mockFetchUsageData = vi.spyOn(usage, 'fetchUsageData');
});
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
{
expected: true,
@@ -47,7 +56,6 @@ describe('usage prefetch', () => {
});
it('fetches usage data once when at least one usage widget exists', async () => {
mockFetchUsageData.mockClear();
mockFetchUsageData.mockResolvedValue({ sessionUsage: 12.3 });
const lines = makeLines(
@@ -62,8 +70,6 @@ describe('usage prefetch', () => {
});
it('does not fetch usage data when no usage widgets exist', async () => {
mockFetchUsageData.mockClear();
const lines = makeLines(
[{ id: '1', type: 'model' }],
[{ id: '2', type: 'git-branch' }]
+17 -11
View File
@@ -1,4 +1,5 @@
import {
afterEach,
beforeEach,
describe,
expect,
@@ -7,27 +8,32 @@ import {
} from 'vitest';
import type { BlockMetrics } from '../../types';
import { getCachedBlockMetrics } from '../jsonl';
import * as jsonl from '../jsonl';
import {
FIVE_HOUR_BLOCK_MS,
SEVEN_DAY_WINDOW_MS,
SEVEN_DAY_WINDOW_MS
} from '../usage-types';
import {
formatUsageDuration,
getUsageWindowFromResetAt,
getWeeklyUsageWindowFromResetAt,
resolveUsageWindowWithFallback,
resolveWeeklyUsageWindow
} from '../usage';
vi.mock('../jsonl', () => ({ getCachedBlockMetrics: vi.fn() }));
const mockGetCachedBlockMetrics = getCachedBlockMetrics as unknown as {
mock: { calls: unknown[][] };
mockReturnValue: (value: BlockMetrics | null) => void;
};
} from '../usage-windows';
describe('usage window helpers', () => {
let mockGetCachedBlockMetrics: {
mock: { calls: unknown[][] };
mockReturnValue: (value: BlockMetrics | null) => void;
};
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
mockGetCachedBlockMetrics = vi.spyOn(jsonl, 'getCachedBlockMetrics');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('parses usage reset timestamp into elapsed and remaining metrics', () => {
+15 -16
View File
@@ -1,4 +1,5 @@
import {
afterEach,
beforeEach,
describe,
expect,
@@ -9,32 +10,30 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
formatUsageDuration,
getUsageErrorMessage,
resolveUsageWindowWithFallback
} from '../../utils/usage';
import * as usage from '../../utils/usage';
import type { UsageWindowMetrics } from '../../utils/usage-types';
import { BlockResetTimerWidget } from '../BlockResetTimer';
import { runUsageTimerEditorSuite } from './helpers/usage-widget-suites';
vi.mock('../../utils/usage', () => ({
formatUsageDuration: vi.fn(),
getUsageErrorMessage: vi.fn(),
resolveUsageWindowWithFallback: vi.fn()
}));
const mockFormatUsageDuration = formatUsageDuration as unknown as { mockReturnValue: (value: string) => void };
const mockGetUsageErrorMessage = getUsageErrorMessage as unknown as { mockReturnValue: (value: string) => void };
const mockResolveUsageWindowWithFallback = resolveUsageWindowWithFallback as unknown as { mockReturnValue: (value: unknown) => void };
function render(widget: BlockResetTimerWidget, item: WidgetItem, context: RenderContext = {}): string | null {
return widget.render(item, context, DEFAULT_SETTINGS);
}
describe('BlockResetTimerWidget', () => {
let mockFormatUsageDuration: { mockReturnValue: (value: string) => void };
let mockGetUsageErrorMessage: { mockReturnValue: (value: string) => void };
let mockResolveUsageWindowWithFallback: { mockReturnValue: (value: UsageWindowMetrics | null) => void };
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
mockFormatUsageDuration = vi.spyOn(usage, 'formatUsageDuration');
mockGetUsageErrorMessage = vi.spyOn(usage, 'getUsageErrorMessage');
mockResolveUsageWindowWithFallback = vi.spyOn(usage, 'resolveUsageWindowWithFallback');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders preview using block-style reset format', () => {
+13 -13
View File
@@ -1,4 +1,5 @@
import {
afterEach,
beforeEach,
describe,
expect,
@@ -9,29 +10,28 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
formatUsageDuration,
resolveUsageWindowWithFallback
} from '../../utils/usage';
import * as usage from '../../utils/usage';
import type { UsageWindowMetrics } from '../../utils/usage-types';
import { BlockTimerWidget } from '../BlockTimer';
import { runUsageTimerEditorSuite } from './helpers/usage-widget-suites';
vi.mock('../../utils/usage', () => ({
formatUsageDuration: vi.fn(),
resolveUsageWindowWithFallback: vi.fn()
}));
const mockFormatUsageDuration = formatUsageDuration as unknown as { mockReturnValue: (value: string) => void };
const mockResolveUsageWindowWithFallback = resolveUsageWindowWithFallback as unknown as { mockReturnValue: (value: unknown) => void };
function render(widget: BlockTimerWidget, item: WidgetItem, context: RenderContext = {}): string | null {
return widget.render(item, context, DEFAULT_SETTINGS);
}
describe('BlockTimerWidget', () => {
let mockFormatUsageDuration: { mockReturnValue: (value: string) => void };
let mockResolveUsageWindowWithFallback: { mockReturnValue: (value: UsageWindowMetrics | null) => void };
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
mockFormatUsageDuration = vi.spyOn(usage, 'formatUsageDuration');
mockResolveUsageWindowWithFallback = vi.spyOn(usage, 'resolveUsageWindowWithFallback');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders elapsed time in time mode', () => {
+12 -2
View File
@@ -1,4 +1,6 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
@@ -7,11 +9,19 @@ import {
import type { RenderContext } from '../../types';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import * as usage from '../../utils/usage';
import { ContextBarWidget } from '../ContextBar';
vi.mock('../../utils/usage', () => ({ makeUsageProgressBar: vi.fn((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`) }));
describe('ContextBarWidget', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(usage, 'makeUsageProgressBar').mockImplementation((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders from context_window data when available', () => {
const context: RenderContext = {
data: {
@@ -13,24 +13,18 @@ import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { CurrentWorkingDirWidget } from '../CurrentWorkingDir';
vi.mock('node:os', () => ({ homedir: vi.fn() }));
const mockHomedir = os.homedir as unknown as {
mockReset: () => void;
mockReturnValue: (value: string) => void;
};
describe('CurrentWorkingDirWidget', () => {
const widget = new CurrentWorkingDirWidget();
const defaultHomeDir = '/Users/alice';
let mockHomedir: { mockReturnValue: (value: string) => void };
beforeEach(() => {
mockHomedir.mockReset();
mockHomedir.mockReturnValue(defaultHomeDir);
vi.restoreAllMocks();
mockHomedir = vi.spyOn(os, 'homedir').mockReturnValue(defaultHomeDir);
});
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});
const createContext = (cwd?: string, isPreview = false): RenderContext => ({
+13 -38
View File
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import * as childProcess from 'child_process';
import os from 'os';
import {
afterEach,
@@ -16,47 +16,22 @@ import type {
import { DEFAULT_SETTINGS } from '../../types/Settings';
import { FreeMemoryWidget } from '../FreeMemory';
vi.mock('child_process', () => ({ execSync: vi.fn() }));
vi.mock('os', () => {
const mockOs = {
totalmem: vi.fn(),
freemem: vi.fn(),
platform: vi.fn()
};
return {
default: mockOs,
...mockOs
};
});
const mockTotalmem = os.totalmem as unknown as {
mockReturnValue: (value: number) => void;
mockReset: () => void;
};
const mockFreemem = os.freemem as unknown as {
mockReturnValue: (value: number) => void;
mockReset: () => void;
};
const mockPlatform = os.platform as unknown as {
mockReturnValue: (value: NodeJS.Platform) => void;
mockReset: () => void;
};
const mockExecSync = execSync as unknown as {
mockReturnValue: (value: string) => void;
mockImplementation: (impl: () => never) => void;
mockReset: () => void;
};
describe('FreeMemoryWidget', () => {
const widget = new FreeMemoryWidget();
let mockTotalmem: { mockReturnValue: (value: number) => void };
let mockFreemem: { mockReturnValue: (value: number) => void };
let mockPlatform: { mockReturnValue: (value: NodeJS.Platform) => void };
let mockExecSync: {
mockImplementation: (fn: () => never) => void;
mockReturnValue: (value: string) => void;
};
beforeEach(() => {
vi.clearAllMocks();
mockTotalmem.mockReset();
mockFreemem.mockReset();
mockPlatform.mockReset();
mockExecSync.mockReset();
vi.restoreAllMocks();
mockTotalmem = vi.spyOn(os, 'totalmem');
mockFreemem = vi.spyOn(os, 'freemem');
mockPlatform = vi.spyOn(os, 'platform');
mockExecSync = vi.spyOn(childProcess, 'execSync');
});
afterEach(() => {
+4 -9
View File
@@ -1,4 +1,4 @@
import { readFileSync } from 'fs';
import * as fs from 'fs';
import {
afterEach,
beforeEach,
@@ -15,12 +15,7 @@ import type {
import { DEFAULT_SETTINGS } from '../../types/Settings';
import { SessionNameWidget } from '../SessionName';
vi.mock('fs', () => ({ readFileSync: vi.fn() }));
const mockReadFileSync = readFileSync as unknown as {
mockImplementation: (impl: () => string) => void;
mockReset: () => void;
};
let mockReadFileSync: { mockImplementation: (fn: () => string | never) => void };
function render(transcriptPath: string | undefined, fileContent: string | null, rawValue = false, isPreview = false) {
const widget = new SessionNameWidget();
@@ -47,8 +42,8 @@ function render(transcriptPath: string | undefined, fileContent: string | null,
describe('SessionNameWidget', () => {
beforeEach(() => {
vi.clearAllMocks();
mockReadFileSync.mockReset();
vi.restoreAllMocks();
mockReadFileSync = vi.spyOn(fs, 'readFileSync');
});
afterEach(() => {
+20 -8
View File
@@ -1,4 +1,6 @@
import {
afterEach,
beforeEach,
describe,
vi
} from 'vitest';
@@ -6,27 +8,37 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { getUsageErrorMessage } from '../../utils/usage';
import * as usage from '../../utils/usage';
import { SessionUsageWidget } from '../SessionUsage';
import { runUsagePercentWidgetSuite } from './helpers/usage-widget-suites';
vi.mock('../../utils/usage', () => ({
getUsageErrorMessage: vi.fn(),
makeUsageProgressBar: vi.fn((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`)
}));
const mockGetUsageErrorMessage = getUsageErrorMessage as unknown as { mockReturnValue: (value: string) => void };
let mockGetUsageErrorMessage: { mockReturnValue: (value: string) => void };
const usageErrorMessageMock = {
mockReturnValue(value: string): void {
mockGetUsageErrorMessage.mockReturnValue(value);
}
};
function render(widget: SessionUsageWidget, item: WidgetItem, context: RenderContext = {}): string | null {
return widget.render(item, context, DEFAULT_SETTINGS);
}
describe('SessionUsageWidget', () => {
beforeEach(() => {
vi.restoreAllMocks();
mockGetUsageErrorMessage = vi.spyOn(usage, 'getUsageErrorMessage');
vi.spyOn(usage, 'makeUsageProgressBar').mockImplementation((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`);
});
afterEach(() => {
vi.restoreAllMocks();
});
runUsagePercentWidgetSuite({
baseItem: { id: 'session', type: 'session-usage' },
createWidget: () => new SessionUsageWidget(),
errorMessageMock: mockGetUsageErrorMessage,
errorMessageMock: usageErrorMessageMock,
expectedModifierText: '(short bar, inverted)',
expectedProgress: 'Session: [bar:76.5:16] 76.5%',
expectedRawProgress: '[bar:23.4:32] 23.4%',
+12 -2
View File
@@ -1,4 +1,6 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
@@ -7,8 +9,7 @@ import {
import type { RenderContext } from '../../types';
import { DEFAULT_SETTINGS } from '../../types/Settings';
vi.mock('../../utils/renderer', () => ({ formatTokens: vi.fn((value: number) => `fmt:${value}`) }));
import * as renderer from '../../utils/renderer';
async function loadWidgets() {
const [{ TokensInputWidget }, { TokensOutputWidget }, { TokensCachedWidget }, { TokensTotalWidget }] = await Promise.all([
@@ -27,6 +28,15 @@ async function loadWidgets() {
}
describe('Token widgets', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(renderer, 'formatTokens').mockImplementation((value: number) => `fmt:${value}`);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('use context_window values for input/output and tokenMetrics totals for cached/total', async () => {
const { TokensCachedWidget, TokensInputWidget, TokensOutputWidget, TokensTotalWidget } = await loadWidgets();
const context: RenderContext = {
+15 -16
View File
@@ -1,4 +1,5 @@
import {
afterEach,
beforeEach,
describe,
expect,
@@ -9,32 +10,30 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
formatUsageDuration,
getUsageErrorMessage,
resolveWeeklyUsageWindow
} from '../../utils/usage';
import * as usage from '../../utils/usage';
import type { UsageWindowMetrics } from '../../utils/usage-types';
import { WeeklyResetTimerWidget } from '../WeeklyResetTimer';
import { runUsageTimerEditorSuite } from './helpers/usage-widget-suites';
vi.mock('../../utils/usage', () => ({
formatUsageDuration: vi.fn(),
getUsageErrorMessage: vi.fn(),
resolveWeeklyUsageWindow: vi.fn()
}));
const mockFormatUsageDuration = formatUsageDuration as unknown as { mockReturnValue: (value: string) => void };
const mockGetUsageErrorMessage = getUsageErrorMessage as unknown as { mockReturnValue: (value: string) => void };
const mockResolveWeeklyUsageWindow = resolveWeeklyUsageWindow as unknown as { mockReturnValue: (value: unknown) => void };
function render(widget: WeeklyResetTimerWidget, item: WidgetItem, context: RenderContext = {}): string | null {
return widget.render(item, context, DEFAULT_SETTINGS);
}
describe('WeeklyResetTimerWidget', () => {
let mockFormatUsageDuration: { mockReturnValue: (value: string) => void };
let mockGetUsageErrorMessage: { mockReturnValue: (value: string) => void };
let mockResolveWeeklyUsageWindow: { mockReturnValue: (value: UsageWindowMetrics | null) => void };
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
mockFormatUsageDuration = vi.spyOn(usage, 'formatUsageDuration');
mockGetUsageErrorMessage = vi.spyOn(usage, 'getUsageErrorMessage');
mockResolveWeeklyUsageWindow = vi.spyOn(usage, 'resolveWeeklyUsageWindow');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders preview using weekly reset format', () => {
+20 -8
View File
@@ -1,4 +1,6 @@
import {
afterEach,
beforeEach,
describe,
vi
} from 'vitest';
@@ -6,27 +8,37 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { getUsageErrorMessage } from '../../utils/usage';
import * as usage from '../../utils/usage';
import { WeeklyUsageWidget } from '../WeeklyUsage';
import { runUsagePercentWidgetSuite } from './helpers/usage-widget-suites';
vi.mock('../../utils/usage', () => ({
getUsageErrorMessage: vi.fn(),
makeUsageProgressBar: vi.fn((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`)
}));
const mockGetUsageErrorMessage = getUsageErrorMessage as unknown as { mockReturnValue: (value: string) => void };
let mockGetUsageErrorMessage: { mockReturnValue: (value: string) => void };
const usageErrorMessageMock = {
mockReturnValue(value: string): void {
mockGetUsageErrorMessage.mockReturnValue(value);
}
};
function render(widget: WeeklyUsageWidget, item: WidgetItem, context: RenderContext = {}): string | null {
return widget.render(item, context, DEFAULT_SETTINGS);
}
describe('WeeklyUsageWidget', () => {
beforeEach(() => {
vi.restoreAllMocks();
mockGetUsageErrorMessage = vi.spyOn(usage, 'getUsageErrorMessage');
vi.spyOn(usage, 'makeUsageProgressBar').mockImplementation((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`);
});
afterEach(() => {
vi.restoreAllMocks();
});
runUsagePercentWidgetSuite({
baseItem: { id: 'weekly', type: 'weekly-usage' },
createWidget: () => new WeeklyUsageWidget(),
errorMessageMock: mockGetUsageErrorMessage,
errorMessageMock: usageErrorMessageMock,
expectedModifierText: '(progress bar, inverted)',
expectedProgress: 'Weekly: [bar:57.9:32] 57.9%',
expectedRawProgress: '[bar:42.1:16] 42.1%',