import { describe, it, expect, vi, beforeEach } from 'vitest' // ── mock node:fs/promises + node:os ────────────────────────────────────────── // Hoisted by vitest; history.ts binds to these mocks. const mockReaddir = vi.fn() const mockStat = vi.fn() const mockOpen = vi.fn() const mockHomedir = vi.fn(() => '/home/tester') vi.mock('node:fs/promises', () => ({ default: { readdir: (...a: unknown[]) => mockReaddir(...a), stat: (...a: unknown[]) => mockStat(...a), open: (...a: unknown[]) => mockOpen(...a), }, })) vi.mock('node:os', async (importOriginal) => { const actual = await importOriginal() return { ...actual, default: { ...actual.default, homedir: () => mockHomedir() } } }) const { parseSessionMeta, listSessions } = await import('../src/http/history.js') // ── parseSessionMeta (pure) ─────────────────────────────────────────────────── describe('parseSessionMeta', () => { it('extracts cwd and the first user prompt (string content)', () => { const jsonl = [ JSON.stringify({ type: 'last-prompt', sessionId: 'x' }), JSON.stringify({ type: 'attachment', cwd: '/Users/me/proj', sessionId: 'x' }), JSON.stringify({ type: 'user', message: { role: 'user', content: 'Add monitoring please' } }), ].join('\n') expect(parseSessionMeta(jsonl)).toEqual({ cwd: '/Users/me/proj', preview: 'Add monitoring please' }) }) it('reads the first text block when content is an array', () => { const jsonl = [ JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: [{ type: 'text', text: 'hello there' }] } }), ].join('\n') expect(parseSessionMeta(jsonl)).toEqual({ cwd: '/p', preview: 'hello there' }) }) it('collapses whitespace and truncates long previews to 120 chars', () => { const long = 'a'.repeat(200) const jsonl = JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: `line1\n line2 ${long}` } }) const { preview } = parseSessionMeta(jsonl) expect(preview.length).toBe(120) expect(preview.startsWith('line1 line2 ')).toBe(true) }) it('returns nulls/empty when there is no cwd or user message', () => { expect(parseSessionMeta('')).toEqual({ cwd: null, preview: '' }) expect(parseSessionMeta('not json\n{"type":"summary"}')).toEqual({ cwd: null, preview: '' }) }) }) // ── listSessions (fs traversal, async) ──────────────────────────────────────── describe('listSessions', () => { function mockFile(text: string) { const buf = Buffer.from(text, 'utf8') return { read: vi.fn(async (b: Buffer, off: number, len: number) => { const n = buf.copy(b, off, 0, Math.min(len, buf.length)) return { bytesRead: n } }), close: vi.fn(async () => undefined), } } beforeEach(() => { mockReaddir.mockReset() mockStat.mockReset() mockOpen.mockReset() mockHomedir.mockReturnValue('/home/tester') }) it('returns [] when the projects root is missing', async () => { mockReaddir.mockRejectedValueOnce(new Error('ENOENT')) expect(await listSessions()).toEqual([]) }) it('lists .jsonl sessions newest-first with parsed cwd/preview/project', async () => { mockReaddir .mockResolvedValueOnce(['projA']) // root .mockResolvedValueOnce(['old.jsonl', 'new.jsonl', 'ignore.txt']) // projA mockStat.mockImplementation(async (p: string) => ({ mtimeMs: p.endsWith('new.jsonl') ? 2000 : 1000, })) mockOpen.mockImplementation(async (p: string) => p.endsWith('new.jsonl') ? mockFile(JSON.stringify({ type: 'user', cwd: '/work/newdir', message: { role: 'user', content: 'newest task' } })) : mockFile(JSON.stringify({ type: 'user', cwd: '/work/olddir', message: { role: 'user', content: 'older task' } })), ) const out = await listSessions() expect(out.map((s) => s.id)).toEqual(['new', 'old']) // newest first expect(out[0]).toMatchObject({ id: 'new', cwd: '/work/newdir', project: 'newdir', preview: 'newest task' }) // .txt is skipped expect(out).toHaveLength(2) }) it('skips a project subdir that cannot be read', async () => { mockReaddir .mockResolvedValueOnce(['good', 'bad']) .mockResolvedValueOnce(['s.jsonl']) // good .mockRejectedValueOnce(new Error('EACCES')) // bad mockStat.mockResolvedValue({ mtimeMs: 1000 }) mockOpen.mockResolvedValue(mockFile(JSON.stringify({ type: 'user', cwd: '/g', message: { role: 'user', content: 'hi' } }))) const out = await listSessions() expect(out).toHaveLength(1) expect(out[0]!.id).toBe('s') }) it('respects the limit', async () => { mockReaddir.mockResolvedValueOnce(['p']).mockResolvedValueOnce(['a.jsonl', 'b.jsonl', 'c.jsonl']) mockStat.mockImplementation(async (p: string) => ({ mtimeMs: p.charCodeAt(p.length - 7) })) mockOpen.mockResolvedValue(mockFile(JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: 'x' } }))) const out = await listSessions(2) expect(out).toHaveLength(2) }) })