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 { 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 { 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 { 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 { 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') }) })