chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
function runCli(args: string[], home: string) {
|
||||
return spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: join(home, '.claude'),
|
||||
HOME: home,
|
||||
TZ: 'UTC',
|
||||
},
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
}
|
||||
|
||||
function userLine(sessionId: string, timestamp: string): string {
|
||||
return JSON.stringify({
|
||||
type: 'user',
|
||||
sessionId,
|
||||
timestamp,
|
||||
message: { role: 'user', content: 'do the thing' },
|
||||
})
|
||||
}
|
||||
|
||||
function assistantEditLine(sessionId: string, timestamp: string, messageId: string): string {
|
||||
// Includes a tool_use of `Edit` so the parser flags this turn as hasEdits=true.
|
||||
// Single edit-turn with no retry (one assistant message in the turn) → counts
|
||||
// as one oneShotTurn.
|
||||
return JSON.stringify({
|
||||
type: 'assistant',
|
||||
sessionId,
|
||||
timestamp,
|
||||
message: {
|
||||
id: messageId,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
model: 'claude-sonnet-4-5',
|
||||
content: [
|
||||
{ type: 'text', text: 'editing' },
|
||||
{ type: 'tool_use', id: 'tu-1', name: 'Edit', input: { file_path: '/tmp/x', old_string: 'a', new_string: 'b' } },
|
||||
],
|
||||
usage: { input_tokens: 1000, output_tokens: 100 },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function assistantNoEditLine(sessionId: string, timestamp: string, messageId: string): string {
|
||||
// No edit tool — this turn does not count toward editTurns/oneShotTurns,
|
||||
// but does count toward `turns` and `calls`.
|
||||
return JSON.stringify({
|
||||
type: 'assistant',
|
||||
sessionId,
|
||||
timestamp,
|
||||
message: {
|
||||
id: messageId,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
model: 'claude-sonnet-4-5',
|
||||
content: [{ type: 'text', text: 'just chatting' }],
|
||||
usage: { input_tokens: 200, output_tokens: 30 },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('codeburn report --format json daily[] one-shot fields (issue #279)', () => {
|
||||
it('exposes per-day turns / editTurns / oneShotTurns / oneShotRate', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-json-daily-'))
|
||||
|
||||
try {
|
||||
const projectDir = join(home, '.claude', 'projects', 'app')
|
||||
await mkdir(projectDir, { recursive: true })
|
||||
|
||||
// Day 1 (2026-04-10): one edit-turn (one-shot), one chat-turn
|
||||
// Day 2 (2026-04-11): one edit-turn (one-shot)
|
||||
await writeFile(
|
||||
join(projectDir, 'session.jsonl'),
|
||||
[
|
||||
userLine('s1', '2026-04-10T09:00:00Z'),
|
||||
assistantEditLine('s1', '2026-04-10T09:01:00Z', 'm-d1-edit'),
|
||||
userLine('s1', '2026-04-10T10:00:00Z'),
|
||||
assistantNoEditLine('s1', '2026-04-10T10:01:00Z', 'm-d1-chat'),
|
||||
userLine('s1', '2026-04-11T09:00:00Z'),
|
||||
assistantEditLine('s1', '2026-04-11T09:01:00Z', 'm-d2-edit'),
|
||||
].join('\n'),
|
||||
)
|
||||
|
||||
const result = runCli([
|
||||
'--format', 'json',
|
||||
'--from', '2026-04-10',
|
||||
'--to', '2026-04-11',
|
||||
'--provider', 'claude',
|
||||
], home)
|
||||
|
||||
expect(result.status).toBe(0)
|
||||
|
||||
const report = JSON.parse(result.stdout) as {
|
||||
daily: Array<{
|
||||
date: string
|
||||
cost: number
|
||||
calls: number
|
||||
turns: number
|
||||
editTurns: number
|
||||
oneShotTurns: number
|
||||
oneShotRate: number | null
|
||||
}>
|
||||
}
|
||||
|
||||
expect(report.daily).toHaveLength(2)
|
||||
|
||||
const day1 = report.daily.find(d => d.date === '2026-04-10')
|
||||
expect(day1).toBeDefined()
|
||||
expect(day1!.turns).toBe(2)
|
||||
expect(day1!.editTurns).toBe(1)
|
||||
expect(day1!.oneShotTurns).toBe(1)
|
||||
expect(day1!.oneShotRate).toBe(100)
|
||||
|
||||
const day2 = report.daily.find(d => d.date === '2026-04-11')
|
||||
expect(day2).toBeDefined()
|
||||
expect(day2!.turns).toBe(1)
|
||||
expect(day2!.editTurns).toBe(1)
|
||||
expect(day2!.oneShotTurns).toBe(1)
|
||||
expect(day2!.oneShotRate).toBe(100)
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('reports null oneShotRate when the day has no edit turns', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-json-daily-'))
|
||||
|
||||
try {
|
||||
const projectDir = join(home, '.claude', 'projects', 'app')
|
||||
await mkdir(projectDir, { recursive: true })
|
||||
|
||||
await writeFile(
|
||||
join(projectDir, 'chat-only.jsonl'),
|
||||
[
|
||||
userLine('s2', '2026-04-10T09:00:00Z'),
|
||||
assistantNoEditLine('s2', '2026-04-10T09:01:00Z', 'm-chat-1'),
|
||||
userLine('s2', '2026-04-10T09:30:00Z'),
|
||||
assistantNoEditLine('s2', '2026-04-10T09:31:00Z', 'm-chat-2'),
|
||||
].join('\n'),
|
||||
)
|
||||
|
||||
const result = runCli([
|
||||
'--format', 'json',
|
||||
'--from', '2026-04-10',
|
||||
'--to', '2026-04-10',
|
||||
'--provider', 'claude',
|
||||
], home)
|
||||
|
||||
expect(result.status).toBe(0)
|
||||
const report = JSON.parse(result.stdout) as {
|
||||
daily: Array<{ date: string; turns: number; editTurns: number; oneShotTurns: number; oneShotRate: number | null }>
|
||||
}
|
||||
const day = report.daily.find(d => d.date === '2026-04-10')!
|
||||
expect(day.turns).toBe(2)
|
||||
expect(day.editTurns).toBe(0)
|
||||
expect(day.oneShotTurns).toBe(0)
|
||||
// null, not 0 — the rate is undefined when no edits happened, and a
|
||||
// chat-only day would otherwise read as 0% one-shot which is misleading.
|
||||
expect(day.oneShotRate).toBeNull()
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('filters a single review day with --day', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-json-day-'))
|
||||
|
||||
try {
|
||||
const projectDir = join(home, '.claude', 'projects', 'app')
|
||||
await mkdir(projectDir, { recursive: true })
|
||||
|
||||
// runCli pins TZ=UTC, so these exact boundaries exercise the inclusive
|
||||
// start/end of the selected calendar day without local-offset drift.
|
||||
await writeFile(
|
||||
join(projectDir, 'day-selector.jsonl'),
|
||||
[
|
||||
userLine('day-before', '2026-04-09T23:59:00Z'),
|
||||
assistantNoEditLine('day-before', '2026-04-09T23:59:30Z', 'm-before'),
|
||||
userLine('day-selected', '2026-04-10T00:00:00Z'),
|
||||
assistantEditLine('day-selected', '2026-04-10T23:59:59Z', 'm-selected'),
|
||||
userLine('day-after', '2026-04-11T00:00:00Z'),
|
||||
assistantNoEditLine('day-after', '2026-04-11T00:00:30Z', 'm-after'),
|
||||
].join('\n'),
|
||||
)
|
||||
|
||||
const result = runCli([
|
||||
'--format', 'json',
|
||||
'--day', '2026-04-10',
|
||||
'--provider', 'claude',
|
||||
], home)
|
||||
|
||||
expect(result.status).toBe(0)
|
||||
const report = JSON.parse(result.stdout) as {
|
||||
period: string
|
||||
periodKey: string
|
||||
daily: Array<{ date: string; calls: number; editTurns: number }>
|
||||
projects: Array<{ sessions: number; calls: number }>
|
||||
}
|
||||
|
||||
expect(report.period).toBe('Day (2026-04-10)')
|
||||
expect(report.periodKey).toBe('day')
|
||||
expect(report.daily.map(d => d.date)).toEqual(['2026-04-10'])
|
||||
expect(report.daily[0]?.calls).toBe(1)
|
||||
expect(report.daily[0]?.editTurns).toBe(1)
|
||||
expect(report.projects[0]?.sessions).toBe(1)
|
||||
expect(report.projects[0]?.calls).toBe(1)
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects --day combined with --from/--to', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-json-day-'))
|
||||
|
||||
try {
|
||||
const result = runCli([
|
||||
'--format', 'json',
|
||||
'--day', '2026-04-10',
|
||||
'--from', '2026-04-10',
|
||||
'--provider', 'claude',
|
||||
], home)
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('--day cannot be combined with --from or --to')
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user