Added new novel strategy

This commit is contained in:
qwsdcvghyu89
2026-06-17 08:11:21 +10:00
parent 5ac2ba6fb5
commit b2e268c6fc
3 changed files with 105 additions and 6 deletions
+80 -3
View File
@@ -1,9 +1,13 @@
import { describe, test, expect, beforeEach } from 'bun:test';
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Hono } from 'hono';
import { ResourceRegistry } from '../src/services/registry.ts';
import { createNovelRouter } from '../src/routes/novel.ts';
import { Cache } from '../src/services/cache.ts';
import { createNovelRouter, registerNovelFromConfig } from '../src/routes/novel.ts';
import type { ChapterView } from '../src/types.ts';
import type { AppConfig } from '../src/config/types.ts';
import type { AppConfig, NovelDefinition } from '../src/config/types.ts';
// Cache is not exercised by route tests — pass a stub that satisfies the type
const stubCache = {} as import('../src/services/cache.ts').Cache;
@@ -173,3 +177,76 @@ describe('POST /qa/requests/novel/new', () => {
expect(res.status).toBe(409);
});
});
describe('registerNovelFromConfig — increment strategy', () => {
const DEF: NovelDefinition = {
slug: 'increment-novel',
type: 'web-scraped',
source: {
urlTemplate: 'https://example.com/chapter-{n}',
startIndex: 1,
},
selectors: { content: { scope: '#content' } },
download: { parallelism: 1, minContentBytes: 5 },
};
let tmpDir: string;
let incRegistry: ResourceRegistry;
let incCache: Cache;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'qa-inc-test-'));
incRegistry = new ResourceRegistry();
incCache = new Cache(tmpDir);
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
test('registers with startsAt = startIndex and endsAt = MAX_SAFE_INTEGER', () => {
const id = registerNovelFromConfig(DEF, incRegistry, incCache, stubAppConfig);
const res = incRegistry.get(id);
expect(res.startsAt).toBe(1);
expect(res.endsAt).toBe(Number.MAX_SAFE_INTEGER);
expect(res.slug).toBe('increment-novel');
});
test('fetch generates URLs from template and downloads chapters', async () => {
const id = registerNovelFromConfig(DEF, incRegistry, incCache, stubAppConfig);
const spy = spyOn(globalThis, 'fetch').mockImplementation(() =>
Promise.resolve(
new Response('<html><body><div id="content">Chapter text content here.</div></body></html>', { status: 200 }),
),
);
const view = await incRegistry.get(id).fetch(1, 3);
expect(view.min).toBe(1);
expect(view.max).toBe(3);
expect(view.chapters).toHaveLength(3);
const urls = spy.mock.calls.map(c => c[0] as string);
expect(urls).toContain('https://example.com/chapter-1');
expect(urls).toContain('https://example.com/chapter-2');
expect(urls).toContain('https://example.com/chapter-3');
spy.mockRestore();
});
test('fetch result is served from cache on second call', async () => {
const id = registerNovelFromConfig(DEF, incRegistry, incCache, stubAppConfig);
const spy = spyOn(globalThis, 'fetch').mockImplementation(() =>
Promise.resolve(
new Response('<html><body><div id="content">Cached chapter content.</div></body></html>', { status: 200 }),
),
);
await incRegistry.get(id).fetch(1, 2);
spy.mockReset();
await incRegistry.get(id).fetch(1, 2);
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
});