codeburn/tests/setup/env-isolation.ts
Rick Culpepper (claude) 448d470049 feat(providers): add cline-cli provider for Cline CLI sessions
The Cline CLI (npm `cline`, 3.x) stores sessions as
<sessions>/<id>/<id>.json + <id>.messages.json. The existing `cline`
provider only discovers tasks/<id>/ui_messages.json, so every CLI session
was silently reported as $0.00 — no warning, not even under --verbose.

Adds `cline-cli` as its own provider rather than a third root on `cline`,
leaving the shared Cline-family parser (Roo Code, KiloCode, IBM Bob)
untouched. It mirrors the CLI's own root resolution
(CLINE_SESSION_DATA_DIR -> CLINE_DATA_DIR -> CLINE_DIR -> ~/.cline),
implements probeRoots() so `doctor` can tell "not installed" from "wrong
override", emits one call per assistant message's `metrics` block, and
falls back to the session rollup when a session carries none. The
fallback reads `usage`, not `aggregateUsage`, which folds in spawned
subagents that are themselves separate session directories.

Two supporting changes, both required for CLI costs to report correctly:

- parser.ts re-priced cline-cli calls from tokens because the provider
  was not on the reported-cost allowlist, inflating a real 12-session
  local sample from $1.11 to $3.92.
- session-cache.ts gains the matching PROVIDER_ENV_VARS entry (so a
  changed override invalidates) and a `reported-cost-v1` parse version
  (so sessions cached before the allowlist fix re-parse once instead of
  being re-priced forever).

Cost is treated as metered only when actually present and non-negative,
so a metered $0 stays reported while a missing or negative cost falls
back to token pricing — applied identically on the per-message and
rollup paths. Timestamps promote a seconds-resolution value rather than
silently landing in 1970, matching the guard kiro.ts uses.

CLINE_DIR / CLINE_DATA_DIR / CLINE_SESSION_DATA_DIR are added to the test
env-isolation list so a developer's real sessions cannot bleed into
fixtures.

The VS Code variant discovery bug reported alongside this in #874 is
deliberately NOT fixed here — it shipped in #882.

Verified against 18 real local sessions: 142 calls, 4,934,762 input /
224,561 output tokens, and a cost matching the CLI's own metered total to
the cent. `codeburn doctor` reports "Cline CLI  OK".

Refs: #874
2026-08-04 02:45:16 +03:00

117 lines
4.4 KiB
TypeScript

// 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',
'CLINE_DIR',
'CLINE_DATA_DIR',
'CLINE_SESSION_DATA_DIR',
'CODEX_HOME',
'CODEWHALE_HOME',
'CRUSH_GLOBAL_DATA',
'CODEBUFF_DATA_DIR',
'FACTORY_DIR',
'GOOSE_PATH_ROOT',
'GROK_HOME',
'KIRO_HOME',
'KIMI_SHARE_DIR',
'MUX_ROOT',
'OPENCODE_DATA_DIR',
'OPENCODE_DB_PREFIX',
'QWEN_DATA_DIR',
'VIBE_HOME',
'WARP_DB_PATH',
'ZS_DATA_DIR',
// codeburn override dirs / paths
'CODEBURN_CACHE_DIR',
'CODEBURN_COPILOT_JETBRAINS_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)