mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-24 07:54:18 +00:00
Every CLI spawn on a large corpus pays seconds of fixed cost before any
query work: node boot, a 100MB+ session-cache JSON.parse, the discovery +
fingerprint sweep, and serve-time classification. The desktop app spawns
one CLI per panel fetch, so it pays that cost per panel.
codeburn serve --stdio is the same CLI kept warm: the app holds one child,
sends {id, args} per line, and gets the command's stdout back. Three layers
make it fast, each disabled outside serve so one-shot runs stay byte-exact:
- loadCache memo (session-cache.ts): the parsed cache object is reused
while a stat() shows the file unchanged; saveCache updates it
write-through. A rewrite by another process still forces a fresh read.
- burst reuse (parser.ts, CODEBURN_PARSE_BURST_MS, serve sets 10s): panel
bursts anchor their range ends at their own new Date(), so the exact-key
memo never hits in real traffic; within the window a re-anchored range is
served by trimming the previous parse instead of re-running discovery.
- fresh commander program per request (main.ts buildProgram factory),
because commander option state is sticky across parses.
The server allows only the app's read queries (status/overview/models/
sessions/compare/yield/spend/optimize/audit), refuses everything else
(client falls back to a spawn), serializes requests, and converts
process.exit into a caught signal. The app starts the child once at
startup; requests route through it only when warm, cold-start keeps the
spawn path with its progress events, any serve failure falls back to a
spawn, and three child deaths disable serve for the app run.
Measured on a real 17B-token corpus: panel fetches drop from ~7.4s per
spawn to 5-900ms warm (sessions/spend 5ms, status 898ms). One-shot CLI
output verified byte-identical against the pre-branch baseline.
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
import { spawn, type ChildProcess } from 'child_process'
|
|
import { join } from 'path'
|
|
|
|
// End-to-end protocol test for `codeburn serve --stdio` (the desktop app's
|
|
// resident query server). Runs the real entry through tsx against the
|
|
// test-isolated env (env-isolation.ts points every provider at empty dirs),
|
|
// so requests answer fast and deterministically empty.
|
|
describe('codeburn serve --stdio', () => {
|
|
let child: ChildProcess
|
|
let buffer = ''
|
|
const waiters = new Map<number, (msg: Record<string, unknown>) => void>()
|
|
let readyResolve: () => void
|
|
const ready = new Promise<void>(resolve => { readyResolve = resolve })
|
|
|
|
function request(id: number, args: string[]): Promise<Record<string, unknown>> {
|
|
return new Promise(resolve => {
|
|
waiters.set(id, resolve)
|
|
child.stdin!.write(JSON.stringify({ id, args }) + '\n')
|
|
})
|
|
}
|
|
|
|
function sendRaw(line: string): void {
|
|
child.stdin!.write(line + '\n')
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
child = spawn(process.execPath, ['--import', 'tsx', join(__dirname, '..', 'src', 'cli.ts'), 'serve', '--stdio'], {
|
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
env: { ...process.env },
|
|
})
|
|
child.stdout!.setEncoding('utf8')
|
|
child.stdout!.on('data', (chunk: string) => {
|
|
buffer += chunk
|
|
let idx: number
|
|
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
const line = buffer.slice(0, idx).trim()
|
|
buffer = buffer.slice(idx + 1)
|
|
if (!line) continue
|
|
let msg: Record<string, unknown>
|
|
try { msg = JSON.parse(line) } catch { continue }
|
|
if (msg['ready']) { readyResolve(); continue }
|
|
const waiter = waiters.get(msg['id'] as number)
|
|
if (waiter) { waiters.delete(msg['id'] as number); waiter(msg) }
|
|
}
|
|
})
|
|
await ready
|
|
}, 60_000)
|
|
|
|
afterAll(() => {
|
|
child?.kill('SIGKILL')
|
|
})
|
|
|
|
it('answers an allowed query with the command stdout', async () => {
|
|
const res = await request(1, ['status', '--format', 'menubar-json', '--period', 'today'])
|
|
expect(res['ok']).toBe(true)
|
|
const payload = JSON.parse(res['output'] as string) as { current: { label: string } }
|
|
expect(payload.current.label).toContain('Today')
|
|
}, 60_000)
|
|
|
|
it('isolates option state between requests (no sticky --period)', async () => {
|
|
// The whole reason serve rebuilds the program per request: commander
|
|
// option state is sticky, and a leaked --period would mislabel every
|
|
// later panel.
|
|
const month = await request(2, ['status', '--format', 'menubar-json', '--period', 'month'])
|
|
const today = await request(3, ['status', '--format', 'menubar-json', '--period', 'today'])
|
|
const monthLabel = (JSON.parse(month['output'] as string) as { current: { label: string } }).current.label
|
|
const todayLabel = (JSON.parse(today['output'] as string) as { current: { label: string } }).current.label
|
|
expect(monthLabel).not.toBe(todayLabel)
|
|
expect(todayLabel).toContain('Today')
|
|
}, 60_000)
|
|
|
|
it('refuses commands outside the read allowlist', async () => {
|
|
const res = await request(4, ['currency', 'EUR'])
|
|
expect(res['ok']).toBe(false)
|
|
expect(res['refused']).toBe(true)
|
|
})
|
|
|
|
it('refuses a smuggled positional on an allowed command', async () => {
|
|
const res = await request(5, ['sessions', 'positional-arg'])
|
|
expect(res['ok']).toBe(false)
|
|
expect(res['refused']).toBe(true)
|
|
})
|
|
|
|
it('survives a malformed request line and keeps serving', async () => {
|
|
sendRaw('this is not json')
|
|
const res = await request(6, ['status', '--format', 'menubar-json', '--period', 'today'])
|
|
expect(res['ok']).toBe(true)
|
|
}, 60_000)
|
|
})
|