const { normalizeRefs } = require('../src/prompt/normalizeRefs'); const { buildFullContext, buildHybrid, prefixBeforeCheckpoint } = require('../src/prompt/build'); const { sampleBook } = require('./fixture'); const chapters = [ { chapterNo: 0, title: 'Introduction', startPage: 1, endPage: 1 }, { chapterNo: 1, title: 'Opening', startPage: 2, endPage: 2 }, { chapterNo: 3, title: 'Workshop', startPage: 3, endPage: 4 }, ]; describe('normalizeRefs', () => { it('maps spoken chapter references', () => { expect(normalizeRefs('summarize chapter three', chapters).hints[0].chapterNo).toBe(3); expect(normalizeRefs('the third chapter', chapters).hints[0].chapterNo).toBe(3); expect(normalizeRefs('chapter III', chapters).hints[0].chapterNo).toBe(3); expect(normalizeRefs('the last chapter', chapters).hints[0].chapterNo).toBe(3); expect(normalizeRefs('first chapter', chapters).hints[0].chapterNo).toBe(0); expect(normalizeRefs('the introduction', chapters).hints[0].chapterNo).toBe(0); }); }); describe('prompt prefix', () => { it('keeps the cached prefix identical and puts hints after it', async () => { const logger = { info() {}, warn() {} }; const book = sampleBook(); const first = await buildFullContext({ persona: 'companion', channel: 'chat', userText: 'hello', book, history: [], rollingSummary: '', cacheMinTokens: 4096, cacheTtl: '5m', logger, }); const second = await buildFullContext({ persona: 'companion', channel: 'chat', userText: 'summarize chapter three', book, history: [{ role: 'user', text: 'earlier' }, { role: 'assistant', text: 'answer' }], rollingSummary: 'old', cacheMinTokens: 4096, cacheTtl: '5m', logger, }); const prefixA = JSON.stringify(prefixBeforeCheckpoint(first.system)); const prefixB = JSON.stringify(prefixBeforeCheckpoint(second.system)); expect(prefixA).toBe(prefixB); expect(prefixA).not.toContain('chapter three'); expect(JSON.stringify(second.system)).toContain('chapter three'); expect(second.system.some((block) => block.cachePoint && block.cachePoint.type === 'default')).toBe(true); }); it('skips the checkpoint when the prefix is short', async () => { const events = []; const logger = { info(obj, msg) { events.push(msg || obj); }, warn() {} }; const built = await buildFullContext({ persona: 'companion', channel: 'chat', userText: 'hello', book: sampleBook('Hi'), history: [], rollingSummary: '', cacheMinTokens: 4096, cacheTtl: '5m', logger, }); expect(built.system.some((block) => block.cachePoint)).toBe(false); expect(events).toContain('cache_skipped_below_min'); }); it('keeps source-marker rules inside the cached prefix for both channels', async () => { const logger = { info() {}, warn() {} }; const book = sampleBook(); const base = { persona: 'companion', userText: 'hello', book, history: [], rollingSummary: '', cacheMinTokens: 4096, cacheTtl: '5m', logger, }; const chat = await buildFullContext({ ...base, channel: 'chat' }); const voice = await buildFullContext({ ...base, channel: 'voice' }); const chatPrefix = JSON.stringify(prefixBeforeCheckpoint(chat.system)); const voicePrefix = JSON.stringify(prefixBeforeCheckpoint(voice.system)); expect(chatPrefix).toBe(voicePrefix); expect(chatPrefix).toContain('[[p:47]]'); expect(chatPrefix).not.toContain('Written reply style'); expect(chatPrefix).not.toContain('Spoken reply style'); expect(JSON.stringify(chat.system)).toContain('Markdown is allowed'); expect(JSON.stringify(voice.system)).toContain('in chapter three'); const hybridChat = await buildHybrid({ ...base, channel: 'chat' }); const hybridVoice = await buildHybrid({ ...base, channel: 'voice' }); expect(JSON.stringify(hybridChat.system)).toBe(JSON.stringify(hybridVoice.system)); expect(JSON.stringify(hybridChat.system)).toContain('[[p:47]]'); expect(JSON.stringify(hybridChat.system)).toContain('Cite only pages and chapters'); expect(JSON.stringify(hybridVoice.messages)).toContain('stripped before speech'); expect(JSON.stringify(hybridChat.messages)).toContain('Markdown is allowed'); }); });