codeburn/tests/default-today-period.test.ts
iamtoruk 8f39cc5487 feat: open the interactive dashboard on today, falling back to 7 days when today is empty
The unset default opened on 7 days everywhere. It now opens on today and
only falls back to 7 days when today holds no sessions yet — the decision
is made from the today-scoped slice of the parse the first paint already
runs, so the probe costs a filter, not a second pass.

Explicit selections are untouched: -p/--period, --day, --from/--to, the
TUI period keys, the app's persisted default period, and every one-shot
(--format json, report/sessions/status, the piped non-TTY render) keep
the 7-day default they had.

Fixes #1111
2026-08-23 02:39:41 -07:00

106 lines
3.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mkdtemp, mkdir, writeFile, rm, utimes } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import { assembleDashboardData, renderDashboard } from '../src/dashboard.js'
import { clearSessionCache } from '../src/parser.js'
import { clearLoadCacheMemo } from '../src/session-cache.js'
const DAY_MS = 24 * 60 * 60 * 1000
let tmpDir: string
beforeEach(async () => {
clearSessionCache()
clearLoadCacheMemo()
tmpDir = await mkdtemp(join(tmpdir(), 'default-today-'))
process.env['CLAUDE_CONFIG_DIR'] = tmpDir
process.env['CODEBURN_CACHE_DIR'] = join(tmpDir, 'cache')
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = join(tmpDir, 'desktop-sessions')
})
afterEach(async () => {
clearSessionCache()
clearLoadCacheMemo()
await rm(tmpDir, { recursive: true, force: true })
})
/** One Claude session whose only turn landed `hoursAgo` ago. */
async function writeSession(name: string, hoursAgo: number): Promise<void> {
const dir = join(tmpDir, 'projects', 'proj')
await mkdir(dir, { recursive: true })
const at = new Date(Date.now() - hoursAgo * 60 * 60 * 1000)
const path = join(dir, `${name}.jsonl`)
await writeFile(path, JSON.stringify({
type: 'assistant',
sessionId: name,
timestamp: at.toISOString(),
cwd: '/tmp/proj',
message: {
id: `msg-${name}`, type: 'message', role: 'assistant', model: 'claude-sonnet-4-5',
content: [], usage: { input_tokens: 100, output_tokens: 50 },
},
}) + '\n')
await utimes(path, at, at)
}
/** The interactive first paint with no explicit period: opens on `today`, with
* the fallback armed. */
function assembleAuto(period: 'today' | 'week', autoFallback: boolean) {
return assembleDashboardData(period, 'all', undefined, undefined, null, null, true, autoFallback)
}
describe('unset default period (#1111)', () => {
it('opens on today when today has sessions', async () => {
await writeSession('now', 0)
await writeSession('older', 3 * 24)
const { period, filteredProjects } = await assembleAuto('today', true)
expect(period).toBe('today')
expect(filteredProjects.length).toBe(1)
})
it('falls back to 7 days when today is empty', async () => {
await writeSession('older', 3 * 24)
const { period, filteredProjects } = await assembleAuto('today', true)
expect(period).toBe('week')
expect(filteredProjects.length).toBe(1)
})
it('resolves to 7 days when there is no data at all', async () => {
const { period } = await assembleAuto('today', true)
// Both windows are empty, so the fallback still fires — the point is that it
// resolves to one period deterministically instead of throwing.
expect(period).toBe('week')
})
it('an explicit period is never moved, even when its window is empty', async () => {
await writeSession('older', 3 * 24)
const today = await assembleAuto('today', false)
expect(today.period).toBe('today')
expect(today.filteredProjects.length).toBe(0)
const week = await assembleAuto('week', false)
expect(week.period).toBe('week')
})
})
describe('non-interactive render (#1111)', () => {
it('keeps the 7-day default when stdout is not a TTY', async () => {
await writeSession('now', 0)
const chunks: string[] = []
const write = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
chunks.push(String(chunk))
return true
})
try {
// isTTY is false under vitest, so this is the piped `codeburn` path even
// with the auto flag set by the CLI.
await renderDashboard('week', 'all', 0, undefined, undefined, null, undefined, undefined, true)
} finally {
write.mockRestore()
}
const out = chunks.join('')
expect(out).toContain('[ 7 Days ]')
expect(out).not.toContain('[ Today ]')
})
})