fix: fix and improve test isolation and collision with environment (#530)

* fix: fix and improve test isolation and collision with environment

* docs: remove unnecessary comment

* test(env-isolation): clear CODEBURN_FORCE_MACOS_MAJOR and pin TZ

Two env vars read in src/ were not isolated: CODEBURN_FORCE_MACOS_MAJOR
(now cleared so it cannot leak between tests) and TZ (now pinned to UTC,
since clearing it falls back to the OS zone and would shift date buckets
versus a clean CI runner).

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
This commit is contained in:
Tiago Santos 2026-06-20 12:42:10 +01:00 committed by GitHub
parent 60410a2632
commit 75c32e6d65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 122 additions and 207 deletions

View file

@ -16,9 +16,6 @@ describe('Antigravity CLI statusLine hook installer', () => {
const settingsPath = join(dir, 'settings.json')
const binDir = join(dir, 'bin')
const codeburnPath = join(binDir, process.platform === 'win32' ? 'codeburn.cmd' : 'codeburn')
const oldSettingsPath = process.env['CODEBURN_ANTIGRAVITY_SETTINGS_PATH']
const oldCacheDir = process.env['CODEBURN_CACHE_DIR']
const oldPath = process.env.PATH
await mkdir(binDir, { recursive: true })
await writeFile(codeburnPath, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n')
await chmod(codeburnPath, 0o755)
@ -29,12 +26,6 @@ describe('Antigravity CLI statusLine hook installer', () => {
try {
await run(dir, settingsPath)
} finally {
if (oldSettingsPath === undefined) delete process.env['CODEBURN_ANTIGRAVITY_SETTINGS_PATH']
else process.env['CODEBURN_ANTIGRAVITY_SETTINGS_PATH'] = oldSettingsPath
if (oldCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = oldCacheDir
if (oldPath === undefined) delete process.env.PATH
else process.env.PATH = oldPath
await rm(dir, { recursive: true, force: true })
}
}

View file

@ -45,7 +45,6 @@ beforeEach(() => {
afterEach(async () => {
vi.useRealTimers()
delete process.env['CODEBURN_CACHE_DIR']
if (existsSync(TMP_CACHE_ROOT)) {
await rm(TMP_CACHE_ROOT, { recursive: true, force: true })
}

View file

@ -429,7 +429,6 @@ describe('DeepSeek v4 models resolve to pricing', () => {
})
it('keeps bundled DeepSeek v4 fallback entries when runtime pricing cache is stale', async () => {
const previousCacheDir = process.env['CODEBURN_CACHE_DIR']
const cacheRoot = await mkdtemp(join(tmpdir(), 'codeburn-pricing-cache-'))
try {
@ -455,11 +454,6 @@ describe('DeepSeek v4 models resolve to pricing', () => {
expect(getModelCosts('deepseek-v4-pro')!.inputCostPerToken).toBe(4.35e-7)
expect(getModelCosts('deepseek-v4-flash')!.inputCostPerToken).toBe(1.4e-7)
} finally {
if (previousCacheDir === undefined) {
delete process.env['CODEBURN_CACHE_DIR']
} else {
process.env['CODEBURN_CACHE_DIR'] = previousCacheDir
}
await rm(cacheRoot, { recursive: true, force: true })
await loadPricing()
}

View file

@ -19,8 +19,6 @@ import {
detectBloatedClaudeMd,
detectUnusedMcp,
detectBashBloat,
detectGhostAgents,
detectGhostSkills,
detectGhostCommands,
loadMcpConfigs,
scanJsonlFile,
@ -217,16 +215,6 @@ describe('detectUnusedMcp', () => {
// ============================================================================
describe('detectBashBloat', () => {
const originalEnv = process.env['BASH_MAX_OUTPUT_LENGTH']
beforeEach(() => {
delete process.env['BASH_MAX_OUTPUT_LENGTH']
})
afterAll(() => {
if (originalEnv !== undefined) process.env['BASH_MAX_OUTPUT_LENGTH'] = originalEnv
})
it('flags when env var is unset (uses default 30K)', () => {
const finding = detectBashBloat()
expect(finding).not.toBeNull()

View file

@ -92,16 +92,12 @@ describe.skipIf(!isSqliteAvailable())(
let tmpHome: string
let tmpCache: string
let dbPath: string
let prevHome: string | undefined
let prevCache: string | undefined
beforeEach(async () => {
tmpHome = await mkdtemp(join(tmpdir(), 'cb-otel-agg-home-'))
tmpCache = await mkdtemp(join(tmpdir(), 'cb-otel-agg-cache-'))
dbPath = join(tmpHome, 'agent-traces.db')
prevHome = process.env['HOME']
prevCache = process.env['CODEBURN_CACHE_DIR']
process.env['HOME'] = tmpHome
process.env['CODEBURN_CACHE_DIR'] = tmpCache
@ -116,10 +112,6 @@ describe.skipIf(!isSqliteAvailable())(
afterEach(async () => {
clearSessionCache()
vi.unstubAllEnvs()
if (prevHome === undefined) delete process.env['HOME']
else process.env['HOME'] = prevHome
if (prevCache === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = prevCache
await rm(tmpHome, { recursive: true, force: true })
await rm(tmpCache, { recursive: true, force: true })
})

View file

@ -7,13 +7,9 @@ import { parseAllSessions } from '../src/parser.js'
import type { DateRange } from '../src/types.js'
let tmpDir: string
let originalConfigDir: string | undefined
let originalDesktopSessionsDir: string | undefined
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'claude-cwd-test-'))
originalConfigDir = process.env['CLAUDE_CONFIG_DIR']
originalDesktopSessionsDir = process.env['CODEBURN_DESKTOP_SESSIONS_DIR']
process.env['CLAUDE_CONFIG_DIR'] = tmpDir
// Point desktop sessions at an empty subdir by default so real sessions
// on the developer's machine do not bleed into the unit tests.
@ -21,16 +17,6 @@ beforeEach(async () => {
})
afterEach(async () => {
if (originalConfigDir === undefined) {
delete process.env['CLAUDE_CONFIG_DIR']
} else {
process.env['CLAUDE_CONFIG_DIR'] = originalConfigDir
}
if (originalDesktopSessionsDir === undefined) {
delete process.env['CODEBURN_DESKTOP_SESSIONS_DIR']
} else {
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = originalDesktopSessionsDir
}
await rm(tmpDir, { recursive: true, force: true })
})

View file

@ -10,24 +10,16 @@ import type { DateRange } from '../src/types.js'
let home: string
let cacheDir: string
let previousHome: string | undefined
let previousCacheDir: string | undefined
beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), 'codeburn-gemini-home-'))
cacheDir = await mkdtemp(join(tmpdir(), 'codeburn-gemini-cache-'))
previousHome = process.env['HOME']
previousCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['HOME'] = home
process.env['CODEBURN_CACHE_DIR'] = cacheDir
})
afterEach(async () => {
clearSessionCache()
if (previousHome === undefined) delete process.env['HOME']
else process.env['HOME'] = previousHome
if (previousCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = previousCacheDir
await rm(home, { recursive: true, force: true })
await rm(cacheDir, { recursive: true, force: true })
})

View file

@ -16,7 +16,6 @@ beforeEach(async () => {
afterEach(async () => {
clearSessionCache()
delete process.env['CLAUDE_CONFIG_DIR']
await rm(home, { recursive: true, force: true })
})

View file

@ -20,25 +20,18 @@ function makeRange(): DateRange {
}
let tmpDirs: string[] = []
let originalConfigDir: string | undefined
beforeAll(async () => {
await loadPricing()
})
beforeEach(() => {
originalConfigDir = process.env['CLAUDE_CONFIG_DIR']
setLocalModelSavings({})
setModelAliases({})
})
afterEach(async () => {
delete (Object.prototype as Record<string, unknown>).calls
if (originalConfigDir === undefined) {
delete process.env['CLAUDE_CONFIG_DIR']
} else {
process.env['CLAUDE_CONFIG_DIR'] = originalConfigDir
}
clearSessionCache()
while (tmpDirs.length > 0) {
const d = tmpDirs.pop()

View file

@ -18,8 +18,6 @@ const CWD = '/Users/test/codexonlyproxied'
let tmpDirs: string[] = []
afterEach(async () => {
delete process.env['CODEX_HOME']
delete process.env['CLAUDE_CONFIG_DIR']
while (tmpDirs.length > 0) {
const d = tmpDirs.pop()
if (d) await rm(d, { recursive: true, force: true })

View file

@ -19,8 +19,6 @@ const MERGE_CWD = '/Users/test/proxiedmerge'
let tmpDirs: string[] = []
afterEach(async () => {
delete process.env['CLAUDE_CONFIG_DIR']
delete process.env['CODEX_HOME']
while (tmpDirs.length > 0) {
const d = tmpDirs.pop()
if (d) await rm(d, { recursive: true, force: true })

View file

@ -127,14 +127,12 @@ const makeRange = (): DateRange => ({ start: RANGE_START, end: RANGE_END })
const FIXTURE_CWD = '/private/var/eywa-proxy-fixture/acme'
let tmpDirs: string[] = []
let originalConfigDir: string | undefined
beforeAll(async () => {
await loadPricing()
})
beforeEach(() => {
originalConfigDir = process.env['CLAUDE_CONFIG_DIR']
setProxyPaths([])
setLocalModelSavings({})
setModelAliases({})
@ -143,8 +141,6 @@ beforeEach(() => {
afterEach(async () => {
setProxyPaths([])
if (originalConfigDir === undefined) delete process.env['CLAUDE_CONFIG_DIR']
else process.env['CLAUDE_CONFIG_DIR'] = originalConfigDir
clearSessionCache()
while (tmpDirs.length > 0) {
const d = tmpDirs.pop()

View file

@ -172,15 +172,11 @@ function totalOutput(projects: Awaited<ReturnType<typeof parseAllSessions>>): nu
// ── Common env setup ──────────────────────────────────────────────────────
let tmpHome: string
let tmpCache: string
let prevHome: string | undefined
let prevCache: string | undefined
beforeEach(async () => {
tmpHome = await mkdtemp(join(tmpdir(), 'cb-parser-test-home-'))
tmpCache = await mkdtemp(join(tmpdir(), 'cb-parser-test-cache-'))
prevHome = process.env['HOME']
prevCache = process.env['CODEBURN_CACHE_DIR']
process.env['HOME'] = tmpHome
process.env['CODEBURN_CACHE_DIR'] = tmpCache
@ -194,11 +190,6 @@ afterEach(async () => {
clearSessionCache()
vi.unstubAllEnvs()
if (prevHome === undefined) delete process.env['HOME']
else process.env['HOME'] = prevHome
if (prevCache === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = prevCache
_synthSources = []
await rm(tmpHome, { recursive: true, force: true })

View file

@ -185,7 +185,6 @@ describe('getPlanUsage', () => {
it('keeps the provider-specific parser filter for one active plan', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-usage-test-'))
const previousHome = process.env['HOME']
process.env['HOME'] = dir
try {
@ -217,18 +216,12 @@ describe('getPlanUsage', () => {
expect(usages).toHaveLength(1)
expect(usages[0]?.spentApiEquivalentUsd).toBe(80)
} finally {
if (previousHome === undefined) {
delete process.env['HOME']
} else {
process.env['HOME'] = previousHome
}
await rm(dir, { recursive: true, force: true })
}
})
it('computes multiple active plan usages from one all-provider parse', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-usage-test-'))
const previousHome = process.env['HOME']
process.env['HOME'] = dir
try {
@ -344,11 +337,6 @@ describe('getPlanUsage', () => {
expect(usages.map(usage => usage.plan.provider)).toEqual(['claude', 'codex'])
expect(usages.map(usage => usage.spentApiEquivalentUsd)).toEqual([100, 50])
} finally {
if (previousHome === undefined) {
delete process.env['HOME']
} else {
process.env['HOME'] = previousHome
}
await rm(dir, { recursive: true, force: true })
}
})

View file

@ -29,7 +29,6 @@ describe('plan presets', () => {
describe('plan config persistence', () => {
it('round-trips per-provider plans and clears one provider at a time', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-'))
const previousHome = process.env['HOME']
process.env['HOME'] = dir
try {
@ -74,18 +73,12 @@ describe('plan config persistence', () => {
expect(await readPlan()).toBeUndefined()
expect(await readPlans()).toEqual({})
} finally {
if (previousHome === undefined) {
delete process.env['HOME']
} else {
process.env['HOME'] = previousHome
}
await rm(dir, { recursive: true, force: true })
}
})
it('reads legacy single-plan config as a provider-keyed plan map', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-'))
const previousHome = process.env['HOME']
process.env['HOME'] = dir
try {
@ -107,18 +100,12 @@ describe('plan config persistence', () => {
resetDay: 3,
})
} finally {
if (previousHome === undefined) {
delete process.env['HOME']
} else {
process.env['HOME'] = previousHome
}
await rm(dir, { recursive: true, force: true })
}
})
it('drops a hand-edited all plan when provider-specific plans are present', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-'))
const previousHome = process.env['HOME']
process.env['HOME'] = dir
try {
@ -144,18 +131,12 @@ describe('plan config persistence', () => {
expect(plans.claude).toMatchObject({ id: 'claude-max', provider: 'claude' })
expect(await readPlan()).toMatchObject({ id: 'claude-max', provider: 'claude' })
} finally {
if (previousHome === undefined) {
delete process.env['HOME']
} else {
process.env['HOME'] = previousHome
}
await rm(dir, { recursive: true, force: true })
}
})
it('does not allow an all-provider plan to overlap provider-specific plans', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-'))
const previousHome = process.env['HOME']
process.env['HOME'] = dir
try {
@ -191,11 +172,6 @@ describe('plan config persistence', () => {
})
expect((await readPlans()).claude).toBeUndefined()
} finally {
if (previousHome === undefined) {
delete process.env['HOME']
} else {
process.env['HOME'] = previousHome
}
await rm(dir, { recursive: true, force: true })
}
})

View file

@ -8,18 +8,12 @@ import type { DateRange } from '../src/types.js'
let home: string
let cacheDir: string
let vibeHome: string
let originalHome: string | undefined
let originalCacheDir: string | undefined
let originalVibeHome: string | undefined
let clearParserCache: (() => void) | undefined
beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), 'codeburn-turn-group-home-'))
cacheDir = await mkdtemp(join(tmpdir(), 'codeburn-turn-group-cache-'))
vibeHome = await mkdtemp(join(tmpdir(), 'codeburn-turn-group-vibe-'))
originalHome = process.env['HOME']
originalCacheDir = process.env['CODEBURN_CACHE_DIR']
originalVibeHome = process.env['VIBE_HOME']
process.env['HOME'] = home
process.env['CODEBURN_CACHE_DIR'] = cacheDir
process.env['VIBE_HOME'] = vibeHome
@ -29,12 +23,6 @@ afterEach(async () => {
clearParserCache?.()
clearParserCache = undefined
vi.resetModules()
if (originalHome === undefined) delete process.env['HOME']
else process.env['HOME'] = originalHome
if (originalCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = originalCacheDir
if (originalVibeHome === undefined) delete process.env['VIBE_HOME']
else process.env['VIBE_HOME'] = originalVibeHome
await rm(home, { recursive: true, force: true })
await rm(cacheDir, { recursive: true, force: true })
await rm(vibeHome, { recursive: true, force: true })

View file

@ -263,7 +263,6 @@ describe('antigravity provider helpers', () => {
it('captures exact Antigravity CLI statusLine usage as fallback calls', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-'))
const oldCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['CODEBURN_CACHE_DIR'] = dir
try {
@ -317,15 +316,12 @@ describe('antigravity provider helpers', () => {
expect(calls[0]!.projectPath).toBeUndefined()
expect(calls[0]!.costUSD).toBeGreaterThan(0)
} finally {
if (oldCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = oldCacheDir
await rm(dir, { recursive: true, force: true })
}
})
it('skips statusLine fallback calls when RPC cache already covered the conversation', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-rpc-dedup-'))
const oldCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['CODEBURN_CACHE_DIR'] = dir
try {
@ -354,15 +350,12 @@ describe('antigravity provider helpers', () => {
expect(calls).toEqual([])
} finally {
if (oldCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = oldCacheDir
await rm(dir, { recursive: true, force: true })
}
})
it('skips singleton statusLine snapshots and deltas monotonic usage', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-runs-'))
const oldCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['CODEBURN_CACHE_DIR'] = dir
const basePayload = {
@ -409,15 +402,12 @@ describe('antigravity provider helpers', () => {
])
expect(calls.map(call => call.cachedInputTokens)).toEqual([0, 0])
} finally {
if (oldCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = oldCacheDir
await rm(dir, { recursive: true, force: true })
}
})
it('treats non-monotonic statusLine usage as a new request snapshot', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-reset-'))
const oldCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['CODEBURN_CACHE_DIR'] = dir
const payload = (
@ -458,8 +448,6 @@ describe('antigravity provider helpers', () => {
[200, 30, 500],
])
} finally {
if (oldCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = oldCacheDir
await rm(dir, { recursive: true, force: true })
}
})

View file

@ -18,19 +18,12 @@ type TestDb = {
}
let tmpRoot: string
let originalEnv: string | undefined
beforeEach(async () => {
tmpRoot = await mkdtemp(join(tmpdir(), 'crush-test-'))
originalEnv = process.env['CRUSH_GLOBAL_DATA']
})
afterEach(async () => {
if (originalEnv === undefined) {
delete process.env['CRUSH_GLOBAL_DATA']
} else {
process.env['CRUSH_GLOBAL_DATA'] = originalEnv
}
await rm(tmpRoot, { recursive: true, force: true })
})

View file

@ -18,16 +18,12 @@ import type { DateRange } from '../../src/types.js'
const skipReason = isSqliteAvailable() ? null : 'node:sqlite not available — needs Node 22+; skipping'
let tmpDir: string
let savedBudget: string | undefined
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'cursor-cap-'))
savedBudget = process.env['CODEBURN_CURSOR_MAX_BUBBLES']
})
afterEach(async () => {
if (savedBudget === undefined) delete process.env['CODEBURN_CURSOR_MAX_BUBBLES']
else process.env['CODEBURN_CURSOR_MAX_BUBBLES'] = savedBudget
await rm(tmpDir, { recursive: true, force: true })
})

View file

@ -8,7 +8,6 @@ import { createDevinProvider } from '../../src/providers/devin.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
let tmpDir: string
const originalHome = process.env['HOME']
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'devin-provider-'))
@ -16,8 +15,6 @@ beforeEach(async () => {
})
afterEach(async () => {
if (originalHome === undefined) delete process.env['HOME']
else process.env['HOME'] = originalHome
await rm(tmpDir, { recursive: true, force: true })
})

View file

@ -7,20 +7,12 @@ import { createMistralVibeProvider } from '../../src/providers/mistral-vibe.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
let tmpDir: string
let originalVibeHome: string | undefined
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'mistral-vibe-test-'))
originalVibeHome = process.env['VIBE_HOME']
delete process.env['VIBE_HOME']
})
afterEach(async () => {
if (originalVibeHome === undefined) {
delete process.env['VIBE_HOME']
} else {
process.env['VIBE_HOME'] = originalVibeHome
}
await rm(tmpDir, { recursive: true, force: true })
})

View file

@ -1,4 +1,4 @@
import { describe, it, expect, afterEach, beforeEach } from 'vitest'
import { describe, it, expect, afterEach } from 'vitest'
import { mkdtemp, mkdir, cp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
@ -28,19 +28,9 @@ function makeRange(offsetMs: number): DateRange {
describe('HIGH-1 prototype pollution via unchecked bracket-assign', () => {
const tmpDirs: string[] = []
let originalConfigDir: string | undefined
beforeEach(() => {
originalConfigDir = process.env['CLAUDE_CONFIG_DIR']
})
afterEach(async () => {
delete (Object.prototype as Record<string, unknown>).calls
if (originalConfigDir === undefined) {
delete process.env['CLAUDE_CONFIG_DIR']
} else {
process.env['CLAUDE_CONFIG_DIR'] = originalConfigDir
}
while (tmpDirs.length > 0) {
const d = tmpDirs.pop()
if (d) await rm(d, { recursive: true, force: true })

View file

@ -28,7 +28,6 @@ beforeEach(() => {
})
afterEach(async () => {
delete process.env['CODEBURN_CACHE_DIR']
if (existsSync(TMP_DIR)) await rm(TMP_DIR, { recursive: true })
})
@ -171,11 +170,8 @@ describe('computeEnvFingerprint', () => {
it('changes when env var changes', () => {
const before = computeEnvFingerprint('claude')
const orig = process.env['CLAUDE_CONFIG_DIR']
process.env['CLAUDE_CONFIG_DIR'] = '/tmp/different'
const after = computeEnvFingerprint('claude')
if (orig === undefined) delete process.env['CLAUDE_CONFIG_DIR']
else process.env['CLAUDE_CONFIG_DIR'] = orig
expect(before).not.toBe(after)
})

View file

@ -0,0 +1,110 @@
// Vitest setup file: isolates every test from the developer's shell environment.
//
// codeburn discovers sessions through a long list of provider-specific env
// vars (CLAUDE_CONFIG_DIR, CODEX_HOME, CRUSH_GLOBAL_DATA, …) and via HOME /
// XDG_* / APPDATA / LOCALAPPDATA. Without this file, any value set in the
// developer's shell (e.g. CLAUDE_CONFIG_DIRS=/Users/me/.claude:…) bleeds into
// fixture-based tests: the parser reads the developer's REAL sessions instead
// of the temp-dir fixture, producing nonsense totals and false failures that
// pass on a clean CI runner.
//
// What this file does:
// 1. Mints an empty sandbox temp dir once per worker.
// 2. REDIRECTED vars (HOME / XDG_* / APPDATA / LOCALAPPDATA) point at the
// sandbox so any fallback to homedir() / platform defaults lands in an
// empty filesystem.
// 3. CLEARED vars (every provider's explicit override) are deleted so a test
// that does NOT set one gets "unconfigured" rather than the dev's value.
// 4. PRESERVED vars (PATH, COLUMNS, …) are snapshotted from the dev's shell
// and restored every test. We can't wipe them - Node uses PATH for spawn
// and module resolution, terminal code uses COLUMNS - but a test that
// mutates them shouldn't leak the change into the next test.
// 5. Re-asserts the above before EVERY test (global beforeEach), so a test
// that mutates an env var doesn't leak its value into the next test.
// Tests can freely set process.env['HOME'] = customDir without saving the
// previous value - the next test gets a fresh sandbox baseline.
//
// CAVEAT: env vars set in a test file's beforeAll() get overwritten by this
// file's beforeEach before each test runs. Use beforeEach (not beforeAll) when
// the test body depends on a specific env var value.
import { mkdtempSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { beforeEach } from 'vitest'
const sandbox = mkdtempSync(join(tmpdir(), 'codeburn-test-env-'))
const REDIRECTED = [
'HOME',
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_CACHE_HOME',
'XDG_STATE_HOME',
'APPDATA',
'LOCALAPPDATA',
] as const
const CLEARED = [
// Provider session-discovery dirs
'CLAUDE_CONFIG_DIR',
'CLAUDE_CONFIG_DIRS',
'CODEX_HOME',
'CRUSH_GLOBAL_DATA',
'CODEBUFF_DATA_DIR',
'FACTORY_DIR',
'GOOSE_PATH_ROOT',
'GROK_HOME',
'KIRO_HOME',
'KIMI_SHARE_DIR',
'MUX_ROOT',
'QWEN_DATA_DIR',
'VIBE_HOME',
'WARP_DB_PATH',
'ZS_DATA_DIR',
// codeburn override dirs / paths
'CODEBURN_CACHE_DIR',
'CODEBURN_COPILOT_OTEL_DB',
'CODEBURN_COPILOT_SESSION_STATE_DIR',
'CODEBURN_COPILOT_WS_STORAGE_DIR',
'CODEBURN_DESKTOP_SESSIONS_DIR',
'CODEBURN_MUX_DIR',
'CODEBURN_ANTIGRAVITY_SETTINGS_PATH',
// codeburn behavior toggles (set by the dev to tweak local runs)
'CODEBURN_COPILOT_DISABLE_OTEL',
'CODEBURN_TZ',
'CODEBURN_VERBOSE',
'CODEBURN_CURSOR_MAX_BUBBLES',
'CODEBURN_FORCE_MACOS_MAJOR',
// Provider model/credential overrides
'KIMI_MODEL_NAME',
'AI_GATEWAY_API_KEY',
'VERCEL_OIDC_TOKEN',
// Read by detectBashBloat - a dev's real shell limit must not bleed in
'BASH_MAX_OUTPUT_LENGTH',
] as const
// Snapshotted from the dev's shell and restored every test. These can't be
// wiped (Node needs PATH for spawn / module resolution, dashboard/table layout
// reads COLUMNS) but a test that mutates them shouldn't leak.
const PRESERVED = ['PATH', 'COLUMNS'] as const
const preservedSnapshot = new Map<string, string | undefined>()
for (const key of PRESERVED) preservedSnapshot.set(key, process.env[key])
function applyIsolation(): void {
for (const key of REDIRECTED) process.env[key] = sandbox
for (const key of CLEARED) delete process.env[key]
for (const key of PRESERVED) {
const original = preservedSnapshot.get(key)
if (original === undefined) delete process.env[key]
else process.env[key] = original
}
// Pin the timezone so date grouping is deterministic regardless of the dev's
// shell TZ. Clearing it is not enough (Node falls back to the OS zone); a
// non-UTC TZ would otherwise shift day buckets versus a clean CI runner. A
// test that needs a specific zone can still set process.env.TZ in beforeEach.
process.env.TZ = 'UTC'
}
applyIsolation()
beforeEach(applyIsolation)

View file

@ -1,39 +1,13 @@
import { describe, expect, it, beforeAll, afterAll } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, beforeAll } from 'vitest'
import { buildMenubarPayloadForRange } from '../src/usage-aggregator.js'
import { getDateRange } from '../src/cli-date.js'
import { loadPricing } from '../src/models.js'
describe('buildMenubarPayloadForRange', () => {
// Point HOME / config at an empty temp dir so the payload is built from an
// empty dataset. This keeps the test deterministic and fast regardless of how
// much real session data the developer's machine has for "today" (parsing a
// heavy day previously pushed this past its timeout).
const saved: Record<string, string | undefined> = {}
let tmp: string
beforeAll(async () => {
tmp = await mkdtemp(join(tmpdir(), 'codeburn-agg-test-'))
for (const key of ['HOME', 'CLAUDE_CONFIG_DIR', 'XDG_CONFIG_HOME', 'CODEBURN_CACHE_DIR']) {
saved[key] = process.env[key]
}
process.env['HOME'] = tmp
process.env['CLAUDE_CONFIG_DIR'] = join(tmp, '.claude')
process.env['XDG_CONFIG_HOME'] = join(tmp, '.config')
process.env['CODEBURN_CACHE_DIR'] = join(tmp, 'cache')
await loadPricing()
})
afterAll(async () => {
for (const [key, value] of Object.entries(saved)) {
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
await rm(tmp, { recursive: true, force: true })
})
it('returns a valid payload and skips optimize findings when optimize:false', async () => {
const payload = await buildMenubarPayloadForRange(getDateRange('today'), { provider: 'all', optimize: false })
expect(typeof payload.current.label).toBe('string')

10
vitest.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// Runs once per worker before any test. Scrubs the developer's shell so
// session-discovery env vars (CLAUDE_CONFIG_DIRS, HOME, XDG_*, every
// provider-specific *_HOME) don't bleed real local data into fixtures.
setupFiles: ['./tests/setup/env-isolation.ts'],
},
})