mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-10 17:14:48 +00:00
server.close() only stops new connections; an in-flight fire-and-forget cache save can land a file mid-recursive-rm, surfacing as ENOTEMPTY on slower runners (run #5). fs.rm's built-in retries absorb the window.
85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
|
|
import { tmpdir } from 'os'
|
|
import { join } from 'path'
|
|
import type { AddressInfo } from 'net'
|
|
import type { Server } from 'http'
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { runWebDashboard } from '../src/web-dashboard.js'
|
|
|
|
// Issue #767 item 2: `codeburn context <session>` accepts an 8-char session id
|
|
// prefix (findClaudeSession does a startsWith match), but /api/context/tree
|
|
// additionally required the resolved ref's sessionId to equal the id passed
|
|
// in verbatim -- so the same prefix that works on the CLI 404s through the
|
|
// API. findClaudeSession is the single source of truth for prefix resolution
|
|
// on both surfaces; the API should trust it exactly like the CLI does instead
|
|
// of re-validating for an exact match afterwards.
|
|
const SESSION_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
|
const SESSION_PREFIX = SESSION_ID.slice(0, 8)
|
|
|
|
describe('web dashboard /api/context/tree: session id prefix', () => {
|
|
let server: Server
|
|
let base: string
|
|
let homeDir: string
|
|
let cacheDir: string
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(join(tmpdir(), 'codeburn-context-home-'))
|
|
cacheDir = await mkdtemp(join(tmpdir(), 'codeburn-context-cache-'))
|
|
process.env['HOME'] = homeDir
|
|
process.env['CODEBURN_CACHE_DIR'] = cacheDir
|
|
delete process.env['CLAUDE_CONFIG_DIR']
|
|
|
|
const projectDir = join(homeDir, '.claude', 'projects', '-tmp-fixture-project')
|
|
await mkdir(projectDir, { recursive: true })
|
|
await writeFile(join(projectDir, `${SESSION_ID}.jsonl`), JSON.stringify({
|
|
type: 'assistant',
|
|
sessionId: SESSION_ID,
|
|
timestamp: '2026-07-01T00:00:00.000Z',
|
|
cwd: '/tmp/fixture-project',
|
|
message: {
|
|
id: 'msg-1',
|
|
type: 'message',
|
|
role: 'assistant',
|
|
model: 'claude-sonnet-4-5',
|
|
content: [{ type: 'text', text: 'hi' }],
|
|
usage: { input_tokens: 100, output_tokens: 50 },
|
|
},
|
|
}) + '\n')
|
|
|
|
server = await runWebDashboard({
|
|
period: 'today', provider: 'all', project: [], exclude: [], port: 0, open: false,
|
|
})
|
|
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
// close() only stops new connections; a request handler's fire-and-forget
|
|
// cache save can still land a file mid-recursive-rm, which surfaces as
|
|
// ENOTEMPTY on slower runners. fs.rm's built-in retries absorb exactly
|
|
// that window.
|
|
await rm(homeDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 })
|
|
await rm(cacheDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 })
|
|
})
|
|
|
|
it('resolves a full session id (control case)', async () => {
|
|
const res = await fetch(`${base}/api/context/tree?provider=claude&id=${SESSION_ID}`)
|
|
expect(res.status).toBe(200)
|
|
const payload = await res.json() as { session: { sessionId: string } }
|
|
expect(payload.session.sessionId).toBe(SESSION_ID)
|
|
})
|
|
|
|
it('resolves an 8-char session id prefix, matching the CLI', async () => {
|
|
const res = await fetch(`${base}/api/context/tree?provider=claude&id=${SESSION_PREFIX}`)
|
|
expect(res.status).toBe(200)
|
|
const payload = await res.json() as { session: { sessionId: string } }
|
|
expect(payload.session.sessionId).toBe(SESSION_ID)
|
|
})
|
|
|
|
it('still 404s for an id that matches nothing', async () => {
|
|
const res = await fetch(`${base}/api/context/tree?provider=claude&id=deadbeef`)
|
|
expect(res.status).toBe(404)
|
|
})
|
|
})
|