import { describe, it, expect } from 'vitest'
import { adaptSession, adaptProject, adaptStats, adaptSavedSearch } from './adapters'
import type { SessionDTO, ProjectDTO, StatsDTO, SavedSearchDTO } from './adapters'
function makeDTO(overrides: Partial<SessionDTO> = {}): SessionDTO {
return {
owner: 'bigbes',
tool: 'claude-code',
host: 'laptop',
session_id: 'abc123',
started_at: 1_000_000,
ended_at: 1_001_000,
source_file: '/path/to/session.jsonl',
summary: 'A test session',
turn_count: 5,
tokens_in_total: 1234,
tokens_out_total: 5678,
...overrides,
}
}
describe('adaptSession', () => {
it('started_at=0 → started="1970-01-01T00:00:00.000Z"', () => {
const s = adaptSession(makeDTO({ started_at: 0, ended_at: 0 }))
expect(s.started).toBe('1970-01-01T00:00:00.000Z')
})
it('working_dir absent → cwd === ""', () => {
const s = adaptSession(makeDTO({ working_dir: undefined }))
expect(s.cwd).toBe('')
})
it('model absent → model === undefined (not null)', () => {
const s = adaptSession(makeDTO({ model: undefined }))
expect(s.model).toBeUndefined()
})
it('id is composite ${tool}/${host}/${session_id}', () => {
const s = adaptSession(makeDTO({ tool: 'opencode', host: 'workpc', session_id: 'xyz789' }))
expect(s.id).toBe('opencode/workpc/xyz789')
const parts = s.id.split('/')
expect(parts[0]).toBe('opencode')
expect(parts[1]).toBe('workpc')
expect(parts[2]).toBe('xyz789')
})
it('ended_at === started_at → ended === null', () => {
const ts = 1_000_000
const s = adaptSession(makeDTO({ started_at: ts, ended_at: ts }))
expect(s.ended).toBeNull()
})
it('ended_at > started_at → ended is the ISO of ended_at', () => {
const startedAt = 1_000_000
const endedAt = 1_001_000
const s = adaptSession(makeDTO({ started_at: startedAt, ended_at: endedAt }))
expect(s.ended).toBe(new Date(endedAt * 1000).toISOString())
})
it('sessionId === d.session_id (bare)', () => {
const s = adaptSession(makeDTO())
expect(s.sessionId).toBe('abc123')
const s2 = adaptSession(makeDTO({ session_id: 'xyz789' }))
expect(s2.sessionId).toBe('xyz789')
})
it('composite id and bare sessionId coexist (IV6)', () => {
const s = adaptSession(makeDTO({ tool: 'opencode', host: 'workpc', session_id: 'xyz789' }))
expect(s.id).toBe('opencode/workpc/xyz789')
expect(s.sessionId).toBe('xyz789')
})
})
function makeProjectDTO(overrides: Partial<ProjectDTO> = {}): ProjectDTO {
return {
cwd: '/home/user/code/project',
sessions: 3,
turn_count: 12,
tokens_in_total: 5000,
tokens_out_total: 8000,
last_active: 1_700_000_000,
hosts: ['laptop', 'workpc'],
tools: ['claude-code', 'opencode'],
top_tool: 'claude-code',
...overrides,
}
}
describe('adaptProject', () => {
it('passes through empty arrays unchanged', () => {
const p = adaptProject(makeProjectDTO({ hosts: [], tools: [] }))
expect(p.hosts).toEqual([])
expect(p.tools).toEqual([])
})
it('last_active=0 → lastActive === ""', () => {
const p = adaptProject(makeProjectDTO({ last_active: 0 }))
expect(p.lastActive).toBe('')
})
it('last_active=1700000000 → ISO string "2023-11-14T22:13:20.000Z"', () => {
const p = adaptProject(makeProjectDTO({ last_active: 1_700_000_000 }))
expect(p.lastActive).toBe(new Date(1_700_000_000 * 1000).toISOString())
// Verify the concrete value for determinism
expect(p.lastActive).toBe('2023-11-14T22:13:20.000Z')
})
it('maps snake_case fields to camelCase', () => {
const p = adaptProject(makeProjectDTO())
expect(p.turns).toBe(12)
expect(p.tokensIn).toBe(5000)
expect(p.tokensOut).toBe(8000)
expect(p.topTool).toBe('claude-code')
expect(p.sessions).toBe(3)
expect(p.cwd).toBe('/home/user/code/project')
})
it('passes through hosts and tools arrays', () => {
const p = adaptProject(makeProjectDTO({ hosts: ['laptop'], tools: ['claude-code', 'opencode'] }))
expect(p.hosts).toEqual(['laptop'])
expect(p.tools).toEqual(['claude-code', 'opencode'])
})
})
// ── adaptStats ───────────────────────────────────────────────────────────────
function makeStatsDTO(overrides: Partial<StatsDTO> = {}): StatsDTO {
return {
per_tool: [],
daily: [],
heatmap: [],
top_cwd: [],
hour_of_day: [],
host_split: [],
...overrides,
}
}
describe('adaptStats', () => {
it('empty arrays are preserved (no fabrication)', () => {
const s = adaptStats(makeStatsDTO())
expect(s.perTool).toEqual([])
expect(s.daily).toEqual([])
expect(s.heatmap).toEqual([])
expect(s.topCwd).toEqual([])
expect(s.hourOfDay).toEqual([])
expect(s.hostSplit).toEqual([])
})
it('date_unix=1700000000 → ISO "2023-11-14T22:13:20.000Z" in heatmap', () => {
const s = adaptStats(makeStatsDTO({
heatmap: [{ date_unix: 1700000000, count: 5 }],
}))
expect(s.heatmap[0].date).toBe('2023-11-14T22:13:20.000Z')
expect(s.heatmap[0].count).toBe(5)
})
it('date_unix=1700000000 → ISO in daily', () => {
const s = adaptStats(makeStatsDTO({
daily: [{ date_unix: 1700000000, per_tool: { 'claude-code': 3 } }],
}))
expect(s.daily[0].date).toBe('2023-11-14T22:13:20.000Z')
expect(s.daily[0].perTool).toEqual({ 'claude-code': 3 })
})
it('top-level snake_case → camelCase conversion', () => {
const s = adaptStats(makeStatsDTO({
per_tool: [{ tool: 'claude-code', sessions: 2, turns: 10, tokens_in: 100, tokens_out: 200, daily_sparkline: [1, 2, 3] }],
top_cwd: [{ cwd: '/home/user/code', count: 7 }],
hour_of_day: [{ hour: 14, count: 42 }],
host_split: [{ host: 'laptop', count: 100 }],
}))
expect(s.perTool[0].tool).toBe('claude-code')
expect(s.topCwd[0].cwd).toBe('/home/user/code')
expect(s.hourOfDay[0].hour).toBe(14)
expect(s.hostSplit[0].host).toBe('laptop')
})
it('ToolRollup daily_sparkline passes through unchanged as dailySparkline', () => {
const sparkline = [0, 5, 10, 3, 7]
const s = adaptStats(makeStatsDTO({
per_tool: [{
tool: 'claude-code',
sessions: 1,
turns: 5,
tokens_in: 10,
tokens_out: 20,
daily_sparkline: sparkline,
}],
}))
expect(s.perTool[0].dailySparkline).toEqual(sparkline)
})
})
// ── adaptSavedSearch ─────────────────────────────────────────────────────────
function makeSavedSearchDTO(overrides: Partial<SavedSearchDTO> = {}): SavedSearchDTO {
return {
name: 'recent-errors',
query: 'level:error',
created_at: 1700000000,
updated_at: 1700000000,
...overrides,
}
}
describe('adaptSavedSearch', () => {
it('created_at=1700000000 → createdAt === "2023-11-14T22:13:20.000Z"', () => {
const ss = adaptSavedSearch(makeSavedSearchDTO())
expect(ss.createdAt).toBe('2023-11-14T22:13:20.000Z')
})
it('updated_at=1700000050 → updatedAt is the ISO of that unix epoch', () => {
const ss = adaptSavedSearch(makeSavedSearchDTO({ updated_at: 1700000050 }))
expect(ss.updatedAt).toBe(new Date(1700000050 * 1000).toISOString())
})
it('name and query pass through unchanged', () => {
const ss = adaptSavedSearch(makeSavedSearchDTO({ name: 'my-search', query: 'tool:opencode' }))
expect(ss.name).toBe('my-search')
expect(ss.query).toBe('tool:opencode')
})
})