TalkingBot/ingestion/tests/structure.test.js

100 lines
4.3 KiB
JavaScript

const fs = require('fs/promises');
const path = require('path');
const { formatMarkedText, parseMarkedText, finalizeChapters, detectLayoutChapters, detectChapters, syntheticChapters } = require('../src/structure');
const { buildSamplePdf } = require('./helpers/samplePdf');
const { createLogger } = require('../src/logger');
const logger = createLogger('silent');
function pagesFrom(count) {
return Array.from({ length: count }, (_, index) => ({ page: index + 1, text: `Page ${index + 1} body` }));
}
describe('marked text', () => {
it('round-trips page text and chapter markers', () => {
const pages = [
{ page: 1, text: 'Title page' },
{ page: 2, text: 'Chapter body' },
];
const chapters = [
{ chapterNo: 0, title: 'Front matter', startPage: 1, endPage: 1 },
{ chapterNo: 1, title: 'The River', startPage: 2, endPage: 2 },
];
const marked = formatMarkedText(pages, chapters);
expect(marked.match(/\[Page \d+\]/g)).toEqual(['[Page 1]', '[Page 2]']);
expect(marked).toContain('=== CHAPTER 1: The River ===');
const parsed = parseMarkedText(marked);
expect(parsed.pages.map((page) => page.text)).toEqual(['Title page', 'Chapter body']);
expect(parsed.markers.map((marker) => marker.chapterNo)).toEqual([0, 1]);
expect(formatMarkedText(parsed.pages, chapters)).toBe(marked);
});
});
describe('chapter detection', () => {
it('uses a PDF outline and treats earlier pages as front matter', async () => {
const pdf = await buildSamplePdf();
await fs.mkdir(path.join(__dirname, 'fixtures'), { recursive: true });
await fs.writeFile(path.join(__dirname, 'fixtures/sample-5page.pdf'), pdf);
const pages = pagesFrom(5);
const detected = await detectChapters({ pdfBytes: pdf, blocks: [], pages, llm: null, logger });
expect(detected.chapterDetectionMethod).toBe('outline');
expect(detected.chapters[0]).toMatchObject({ chapterNo: 0, startPage: 1 });
expect(detected.chapters.map((chapter) => chapter.startPage)).toEqual([1, 2, 4]);
expect(detected.chapters[2].endPage).toBe(5);
const starts = detected.chapters.map((chapter) => chapter.startPage);
expect([...starts].sort((a, b) => a - b)).toEqual(starts);
});
it('uses layout headings near the top of the page', () => {
const blocks = [
{
Id: 'title',
BlockType: 'LAYOUT_TITLE',
Page: 3,
Text: 'Chapter 1 The River',
Geometry: { BoundingBox: { Top: 0.08, Left: 0.1, Width: 0.5, Height: 0.03 } },
},
{
Id: 'line',
BlockType: 'LINE',
Page: 3,
Text: 'Chapter 1 The River',
Geometry: { BoundingBox: { Top: 0.08, Left: 0.1, Width: 0.5, Height: 0.03 } },
},
];
const found = detectLayoutChapters(blocks, 6);
const chapters = finalizeChapters(found, 6);
expect(chapters.some((chapter) => chapter.title.includes('River'))).toBe(true);
expect(chapters[0].chapterNo).toBe(0);
expect(chapters[0].endPage).toBe(2);
});
it('asks the model when no outline or headings exist', async () => {
const pages = pagesFrom(4).map((page, index) => ({
...page,
text: index === 1 ? 'Chapter 2 Harbor\nShips leave.' : page.text,
}));
const llm = {
async complete() {
return { data: { chapters: [{ title: 'Harbor', startPage: 2 }] }, usage: { inputTokens: 1, outputTokens: 1 } };
},
};
const detected = await detectChapters({ pdfBytes: null, blocks: [], pages, llm, logger });
expect(detected.chapterDetectionMethod).toBe('llm');
expect(detected.chapters.map((chapter) => chapter.chapterNo)).toEqual([0, 1]);
});
it('builds synthetic sections when nothing else matches', async () => {
const pages = pagesFrom(22);
const detected = await detectChapters({ pdfBytes: null, blocks: [], pages, llm: null, logger });
expect(detected.chapterDetectionMethod).toBe('synthetic');
expect(detected.chapters.every((chapter) => chapter.synthetic)).toBe(true);
expect(detected.chapters[0].startPage).toBe(1);
expect(detected.chapters[detected.chapters.length - 1].endPage).toBe(22);
for (let index = 1; index < detected.chapters.length; index += 1) {
expect(detected.chapters[index].startPage).toBe(detected.chapters[index - 1].endPage + 1);
}
expect(syntheticChapters(22).length).toBeGreaterThan(1);
});
});