mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-22 15:05:16 +00:00
Review of T0 (Electron scaffold) approved with one Important finding + cheap
Minors. All fixes scoped to app/.
- usePolled: replace per-call cancel closure with a generation/epoch counter so
an orphaned in-flight fetch (from refresh() or an interval tick, discarded
cancel handle) can't resolve after a newer fetch and clobber fresh data. New
TDD race test (slow deps-A resolves after fast deps-B; keeps B).
- cli: nvm resolution now scans version dirs descending and takes the first
whose bin actually contains codeburn (was lexicographic max, unverified),
matching CodeburnCLI.swift. Export nodeManagerDirs + hermetic nvm test.
- index.html: tighten CSP connect-src ws: -> ws://localhost:5173.
- preload: import type { Envelope } from main instead of redeclaring it.
- main.test: table-driven channel->argv assertion over all 9 codeburn:* channels
plus a keys check and a non-spawning cliStatus case.
106 lines
5.1 KiB
TypeScript
106 lines
5.1 KiB
TypeScript
// @vitest-environment node
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
// Stub electron so importing main.ts does not require an Electron runtime.
|
|
vi.mock('electron', () => ({
|
|
app: { whenReady: () => Promise.resolve(), on: () => {}, quit: () => {} },
|
|
BrowserWindow: class {},
|
|
ipcMain: { handle: () => {} },
|
|
}))
|
|
|
|
import { createBridgeHandlers } from './main'
|
|
import { CliError } from './cli'
|
|
|
|
function fakeSpawn(result: unknown = { current: { cost: 12.34 } }) {
|
|
const calls: string[][] = []
|
|
const spawnCli = vi.fn(async (args: string[]) => {
|
|
calls.push(args)
|
|
return result
|
|
})
|
|
return { spawnCli, calls }
|
|
}
|
|
|
|
// Every codeburn:* channel with a representative arg tuple → the exact argv it
|
|
// must spawn. cliStatus is the one channel that resolves without spawning.
|
|
const CHANNELS = [
|
|
'codeburn:getOverview',
|
|
'codeburn:getPlans',
|
|
'codeburn:getModels',
|
|
'codeburn:getYield',
|
|
'codeburn:getSpendFlow',
|
|
'codeburn:getDevices',
|
|
'codeburn:getShareStatus',
|
|
'codeburn:getIdentity',
|
|
'codeburn:cliStatus',
|
|
] as const
|
|
|
|
const ARGV_CASES: Array<{ channel: string; args: unknown[]; argv: string[] }> = [
|
|
{ channel: 'codeburn:getOverview', args: ['30days', 'claude'], argv: ['status', '--format', 'menubar-json', '--period', '30days', '--provider', 'claude'] },
|
|
{ channel: 'codeburn:getOverview', args: ['30days', 'all'], argv: ['status', '--format', 'menubar-json', '--period', '30days'] },
|
|
{ channel: 'codeburn:getPlans', args: ['week'], argv: ['status', '--format', 'json', '--period', 'week'] },
|
|
{ channel: 'codeburn:getModels', args: ['week', 'claude', true], argv: ['models', '--format', 'json', '--period', 'week', '--provider', 'claude', '--by-task'] },
|
|
{ channel: 'codeburn:getModels', args: ['week', 'all', false], argv: ['models', '--format', 'json', '--period', 'week'] },
|
|
{ channel: 'codeburn:getYield', args: ['today'], argv: ['yield', '--format', 'json', '--period', 'today'] },
|
|
{ channel: 'codeburn:getSpendFlow', args: ['month', 'openai'], argv: ['spend', '--format', 'flow-json', '--period', 'month', '--provider', 'openai'] },
|
|
{ channel: 'codeburn:getDevices', args: ['week'], argv: ['devices', '--format', 'json', '--period', 'week'] },
|
|
{ channel: 'codeburn:getShareStatus', args: [], argv: ['share', 'status', '--format', 'json'] },
|
|
{ channel: 'codeburn:getIdentity', args: [], argv: ['identity', '--format', 'json'] },
|
|
]
|
|
|
|
describe('createBridgeHandlers (channel → argv for all channels)', () => {
|
|
it('exposes exactly the nine codeburn:* channels', () => {
|
|
const handlers = createBridgeHandlers({ spawnCli: vi.fn(), resolveCodeburnPath: () => null })
|
|
expect(Object.keys(handlers).sort()).toEqual([...CHANNELS].sort())
|
|
})
|
|
|
|
it.each(ARGV_CASES)('$channel with $args spawns the expected argv', async ({ channel, args, argv }) => {
|
|
const { spawnCli, calls } = fakeSpawn()
|
|
const handlers = createBridgeHandlers({ spawnCli, resolveCodeburnPath: () => '/bin/codeburn' })
|
|
const res = await handlers[channel]!(...args)
|
|
expect(calls[0]).toEqual(argv)
|
|
expect(res).toMatchObject({ ok: true })
|
|
})
|
|
|
|
it('codeburn:cliStatus resolves from resolveCodeburnPath without spawning', async () => {
|
|
const spawnCli = vi.fn()
|
|
const handlers = createBridgeHandlers({ spawnCli, resolveCodeburnPath: () => '/opt/homebrew/bin/codeburn' })
|
|
const res = await handlers['codeburn:cliStatus']!()
|
|
expect(spawnCli).not.toHaveBeenCalled()
|
|
expect(res).toEqual({ ok: true, value: { found: true, path: '/opt/homebrew/bin/codeburn' } })
|
|
})
|
|
})
|
|
|
|
describe('createBridgeHandlers (IPC wiring)', () => {
|
|
it('getOverview spawns menubar-json for the period, omitting --provider for "all"', async () => {
|
|
const { spawnCli, calls } = fakeSpawn()
|
|
const handlers = createBridgeHandlers({ spawnCli, resolveCodeburnPath: () => '/bin/codeburn' })
|
|
const res = await handlers['codeburn:getOverview']!('30days', 'all')
|
|
expect(calls[0]).toEqual(['status', '--format', 'menubar-json', '--period', '30days'])
|
|
expect(res).toEqual({ ok: true, value: { current: { cost: 12.34 } } })
|
|
})
|
|
|
|
it('adds --provider and --by-task when requested', async () => {
|
|
const { spawnCli, calls } = fakeSpawn([])
|
|
const handlers = createBridgeHandlers({ spawnCli, resolveCodeburnPath: () => null })
|
|
await handlers['codeburn:getModels']!('week', 'claude', true)
|
|
expect(calls[0]).toEqual(['models', '--format', 'json', '--period', 'week', '--provider', 'claude', '--by-task'])
|
|
})
|
|
|
|
it('returns an error envelope carrying the CliError kind', async () => {
|
|
const spawnCli = vi.fn(async () => {
|
|
throw new CliError('nonzero', 'boom')
|
|
})
|
|
const handlers = createBridgeHandlers({ spawnCli, resolveCodeburnPath: () => '/bin/codeburn' })
|
|
const res = await handlers['codeburn:getYield']!('today')
|
|
expect(res).toEqual({ ok: false, error: { kind: 'nonzero', message: 'boom' } })
|
|
})
|
|
|
|
it('cliStatus reports the resolved binary path', async () => {
|
|
const handlers = createBridgeHandlers({
|
|
spawnCli: vi.fn(),
|
|
resolveCodeburnPath: () => '/opt/homebrew/bin/codeburn',
|
|
})
|
|
const res = await handlers['codeburn:cliStatus']!()
|
|
expect(res).toEqual({ ok: true, value: { found: true, path: '/opt/homebrew/bin/codeburn' } })
|
|
})
|
|
})
|