mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
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.
226 lines
9.2 KiB
TypeScript
226 lines
9.2 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_PLAN_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, // os.homedir() uses USERPROFILE on Windows
|
|
HOMEPATH: home,
|
|
HOMEDRIVE: '',
|
|
},
|
|
encoding: 'utf-8',
|
|
})
|
|
}
|
|
|
|
describe('codeburn plan command', () => {
|
|
it('persists provider-keyed plans and clears on reset', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
const setResult = runCli(['plan', 'set', 'claude-max'], home)
|
|
expect(setResult.status).toBe(0)
|
|
|
|
const setCodexResult = runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home)
|
|
expect(setCodexResult.status).toBe(0)
|
|
|
|
const configPath = join(home, '.config', 'codeburn', 'config.json')
|
|
const configRaw = await readFile(configPath, 'utf-8')
|
|
const config = JSON.parse(configRaw) as { plans?: { claude?: { id?: string; monthlyUsd?: number }; codex?: { id?: string; monthlyUsd?: number } } }
|
|
expect(config.plans?.claude?.id).toBe('claude-max')
|
|
expect(config.plans?.claude?.monthlyUsd).toBe(200)
|
|
expect(config.plans?.codex?.id).toBe('custom')
|
|
expect(config.plans?.codex?.monthlyUsd).toBe(200)
|
|
|
|
const resetResult = runCli(['plan', 'reset'], home)
|
|
expect(resetResult.status).toBe(0)
|
|
|
|
const afterResetRaw = await readFile(configPath, 'utf-8')
|
|
const afterReset = JSON.parse(afterResetRaw) as { plan?: unknown; plans?: unknown }
|
|
expect(afterReset.plan).toBeUndefined()
|
|
expect(afterReset.plans).toBeUndefined()
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('resets one provider without removing other plans', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0)
|
|
expect(runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home).status).toBe(0)
|
|
expect(runCli(['plan', 'reset', '--provider', 'codex'], home).status).toBe(0)
|
|
|
|
const configPath = join(home, '.config', 'codeburn', 'config.json')
|
|
const configRaw = await readFile(configPath, 'utf-8')
|
|
const config = JSON.parse(configRaw) as { plans?: { claude?: { id?: string }; codex?: unknown } }
|
|
expect(config.plans?.claude?.id).toBe('claude-max')
|
|
expect(config.plans?.codex).toBeUndefined()
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('resets the all-provider plan without removing provider-specific plans', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0)
|
|
expect(runCli(['plan', 'reset', '--provider', 'all'], home).status).toBe(0)
|
|
|
|
const configPath = join(home, '.config', 'codeburn', 'config.json')
|
|
const configRaw = await readFile(configPath, 'utf-8')
|
|
const config = JSON.parse(configRaw) as { plans?: { claude?: { id?: string }; all?: unknown } }
|
|
expect(config.plans?.claude?.id).toBe('claude-max')
|
|
expect(config.plans?.all).toBeUndefined()
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('shows all configured plans as json', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0)
|
|
expect(runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home).status).toBe(0)
|
|
|
|
const result = runCli(['plan', '--format', 'json'], home)
|
|
expect(result.status).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as { id?: string; provider?: string; plans?: { claude?: { id?: string }; codex?: { id?: string } } }
|
|
expect(payload.id).toBe('claude-max')
|
|
expect(payload.provider).toBe('claude')
|
|
expect(payload.plans?.claude?.id).toBe('claude-max')
|
|
expect(payload.plans?.codex?.id).toBe('custom')
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('filters shown plans by provider', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0)
|
|
expect(runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home).status).toBe(0)
|
|
|
|
const result = runCli(['plan', '--format', 'json', '--provider', 'codex'], home)
|
|
expect(result.status).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as { id?: string; provider?: string; plans?: unknown }
|
|
expect(payload.id).toBe('custom')
|
|
expect(payload.provider).toBe('codex')
|
|
expect(payload.plans).toMatchObject({ codex: { id: 'custom' } })
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('rejects all-provider scope for preset plans', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
const result = runCli(['plan', 'set', 'claude-max', '--provider', 'all'], home)
|
|
expect(result.status).toBe(1)
|
|
expect(result.stderr).toContain('omit --provider or use --provider claude')
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('shows invalid reset-day value in error output', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
const result = runCli(['plan', 'set', 'claude-max', '--reset-day', '99'], home)
|
|
expect(result.status).toBe(1)
|
|
expect(result.stderr).toContain('--reset-day must be an integer from 1 to 28; got 99.')
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('sets copilot-pro as 1500 credits / $15 equivalent', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
const result = runCli(['plan', 'set', 'copilot-pro'], home)
|
|
expect(result.status).toBe(0)
|
|
|
|
const configPath = join(home, '.config', 'codeburn', 'config.json')
|
|
const config = JSON.parse(await readFile(configPath, 'utf-8')) as {
|
|
plans?: { copilot?: { id?: string; monthlyCredits?: number; monthlyUsd?: number; provider?: string; resetDay?: number } }
|
|
}
|
|
expect(config.plans?.copilot).toMatchObject({
|
|
id: 'copilot-pro',
|
|
monthlyCredits: 1500,
|
|
monthlyUsd: 15,
|
|
provider: 'copilot',
|
|
resetDay: 1,
|
|
})
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
it('writes custom copilot credits and rejects mixed units', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
const ok = runCli(['plan', 'set', 'custom', '--credits', '20000', '--provider', 'copilot'], home)
|
|
expect(ok.status).toBe(0)
|
|
const configPath = join(home, '.config', 'codeburn', 'config.json')
|
|
const config = JSON.parse(await readFile(configPath, 'utf-8')) as {
|
|
plans?: { copilot?: { monthlyCredits?: number; monthlyUsd?: number; provider?: string } }
|
|
}
|
|
expect(config.plans?.copilot).toMatchObject({
|
|
monthlyCredits: 20000,
|
|
monthlyUsd: 200,
|
|
provider: 'copilot',
|
|
})
|
|
|
|
const mixed = runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'copilot'], home)
|
|
expect(mixed.status).not.toBe(0)
|
|
expect(mixed.stderr).toContain('units mixed')
|
|
|
|
const wrongProvider = runCli(['plan', 'set', 'custom', '--credits', '20000', '--provider', 'claude'], home)
|
|
expect(wrongProvider.status).not.toBe(0)
|
|
expect(wrongProvider.stderr).toContain('--credits is only valid with --provider copilot')
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS)
|
|
|
|
// #1199: the JSON plan block must say how complete the credit total is, not
|
|
// just carry a bare creditsIncomplete boolean.
|
|
it('emits the credit completeness fields in status JSON', async () => {
|
|
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-'))
|
|
|
|
try {
|
|
expect(runCli(['plan', 'set', 'copilot-pro'], home).status).toBe(0)
|
|
const status = runCli(['status', '--provider', 'copilot', '--format', 'json', '--period', 'month', '--no-optimize', '--no-timeline'], home)
|
|
expect(status.status).toBe(0)
|
|
const copilot = (JSON.parse(status.stdout) as {
|
|
plans?: { copilot?: Record<string, unknown> }
|
|
}).plans?.copilot
|
|
expect(copilot).toMatchObject({
|
|
spentCredits: 0,
|
|
estimatedCredits: 0,
|
|
creditRatedCalls: 0,
|
|
creditUnratedCalls: 0,
|
|
creditsIncomplete: false,
|
|
creditsNote: 'No Copilot requests in this period.',
|
|
})
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true })
|
|
}
|
|
}, CLI_PLAN_TIMEOUT_MS * 3)
|
|
})
|