import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { mkdtempSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { Cache } from '../src/services/cache.ts'; import type { TextFragment } from '../src/types.ts'; let tmpDir: string; let cache: Cache; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'qa-cache-test-')); cache = new Cache(tmpDir); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); describe('cache — chapters', () => { test('returns null on a cold cache', () => { expect(cache.tryGetChapters(1, 0, 5)).toBeNull(); }); test('returns null when a chapter in range is missing', () => { cache.saveChapters(1, [ { index: 0, content: 'Chapter zero content here' }, // index 1 is intentionally missing { index: 2, content: 'Chapter two content here' }, ]); expect(cache.tryGetChapters(1, 0, 2)).toBeNull(); }); test('returns null for invalid range (min > max)', () => { expect(cache.tryGetChapters(1, 5, 2)).toBeNull(); }); test('saves and retrieves plain-text chapters', () => { cache.saveChapters(1, [ { index: 0, content: 'Line one\nLine two' }, { index: 1, content: 'Chapter one content here' }, ]); const view = cache.tryGetChapters(1, 0, 1); expect(view).not.toBeNull(); expect(view!.id).toBe(1); expect(view!.min).toBe(0); expect(view!.max).toBe(1); expect(view!.chapters).toHaveLength(2); expect(view!.chapters[0]!.hasStyle).toBe(false); }); test('detects styled (JSON fragment) chapters', () => { const fragments: TextFragment[] = [{ body: 'Hello world', style: undefined }]; cache.saveChapters(1, [{ index: 0, content: JSON.stringify(fragments) }]); const view = cache.tryGetChapters(1, 0, 0); expect(view).not.toBeNull(); expect(view!.chapters[0]!.hasStyle).toBe(true); }); test('returns null when a chapter file is too short', () => { cache.saveChapters(1, [{ index: 0, content: 'hi' }]); // < 10 bytes expect(cache.tryGetChapters(1, 0, 0)).toBeNull(); }); test('handles single-chapter range', () => { cache.saveChapters(42, [{ index: 7, content: 'Single chapter content ok' }]); const view = cache.tryGetChapters(42, 7, 7); expect(view).not.toBeNull(); expect(view!.chapters).toHaveLength(1); }); }); describe('cache — TOC', () => { test('returns null on a cold cache', () => { expect(cache.tryGetToc(1)).toBeNull(); }); test('round-trips a TOC', () => { const urls = [ 'https://example.com/chapter-1', 'https://example.com/chapter-2', 'https://example.com/chapter-3', ]; cache.saveToc(1, urls); expect(cache.tryGetToc(1)).toEqual(urls); }); test('returns null for an empty TOC file', () => { cache.saveToc(1, []); expect(cache.tryGetToc(1)).toBeNull(); }); test('overwrites an existing TOC on re-save', () => { cache.saveToc(1, ['https://example.com/old']); cache.saveToc(1, ['https://example.com/new-1', 'https://example.com/new-2']); expect(cache.tryGetToc(1)).toEqual([ 'https://example.com/new-1', 'https://example.com/new-2', ]); }); });