codeburn/tests/cli-model-savings.test.ts
iamtoruk 62cb4f8630 test: isolate the real home on Windows and make the CLI suite pass there
os.homedir() reads USERPROFILE on Windows and never HOME, so the
isolation setup and every test that moved home were still resolving the
developer's real home directory. Fixtures came back empty, real sessions
leaked into counts, and the code under test wrote plans, receipts and a
sync ledger into the real ~/.config/codeburn during a run.

USERPROFILE now joins the redirected variables and a shared setHome()
helper moves HOME and USERPROFILE together; every spawn env carries both.
The remaining Windows-only failures were POSIX-shaped fixtures: chmod
tricks that only toggle a read-only attribute on NTFS, pid 1 as an
always-alive lock holder, splitting paths on '/', bare tar, mode-bit
expectations, and per-file timeouts too short for the cost of a tsx spawn.
Each gets a platform-aware equivalent so nothing is skipped on Windows and
the POSIX branches stay byte-for-byte what they were.

Full run on Windows: 267 files passed, 2 skipped, 3592 tests passed.
2026-09-02 13:43:57 -07:00

76 lines
2.6 KiB
TypeScript

import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { describe, it, expect } from 'vitest'
const CLI_TIMEOUT_MS = 30_000
function runCli(args: string[], home: string) {
return spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], {
cwd: process.cwd(),
env: {
...process.env,
HOME: home,
USERPROFILE: home,
HOMEPATH: home,
HOMEDRIVE: '',
},
encoding: 'utf-8',
})
}
function readConfig(home: string): Promise<Record<string, unknown>> {
return readFile(join(home, '.config', 'codeburn', 'config.json'), 'utf-8')
.then(raw => JSON.parse(raw) as Record<string, unknown>)
}
describe('codeburn model-savings command', () => {
it('saves, lists, and removes a local-model savings mapping', async () => {
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-savings-'))
try {
const set = runCli(['model-savings', 'llama3.1:8b', 'gpt-4o'], home)
expect(set.status).toBe(0)
expect(set.stdout).toContain('llama3.1:8b -> gpt-4o')
const saved = await readConfig(home)
expect(saved.localModelSavings).toEqual({ 'llama3.1:8b': 'gpt-4o' })
const list = runCli(['model-savings', '--list'], home)
expect(list.status).toBe(0)
expect(list.stdout).toContain('llama3.1:8b -> gpt-4o')
const remove = runCli(['model-savings', '--remove', 'llama3.1:8b'], home)
expect(remove.status).toBe(0)
const after = await readConfig(home)
expect(after.localModelSavings).toBeUndefined()
} finally {
await rm(home, { recursive: true, force: true })
}
}, CLI_TIMEOUT_MS)
it('warns when the same model is also configured in modelAliases', async () => {
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-savings-'))
try {
expect(runCli(['model-alias', 'llama3.1:8b', 'gpt-4o'], home).status).toBe(0)
const set = runCli(['model-savings', 'llama3.1:8b', 'gpt-4o'], home)
expect(set.status).toBe(0)
expect(set.stdout).toContain('savings take precedence')
} finally {
await rm(home, { recursive: true, force: true })
}
}, CLI_TIMEOUT_MS)
it('rejects a remove for an unknown mapping', async () => {
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-savings-'))
try {
const result = runCli(['model-savings', '--remove', 'unknown:1b'], home)
expect(result.status).toBe(1)
expect(result.stderr).toContain('No savings mapping found')
} finally {
await rm(home, { recursive: true, force: true })
}
}, CLI_TIMEOUT_MS)
})