diff --git a/packages/web/server/lib/text/summarization.js b/packages/web/server/lib/text/summarization.js index 3b9f2fcb..c6592357 100644 --- a/packages/web/server/lib/text/summarization.js +++ b/packages/web/server/lib/text/summarization.js @@ -115,6 +115,13 @@ function sanitizeByMode(text, mode) { return sanitizeForTTS(text); } +function clampToMaxLength(text, maxLength) { + if (!text) return ''; + const limit = Number.isFinite(maxLength) ? Math.max(0, Math.floor(maxLength)) : Infinity; + if (text.length <= limit) return text; + return text.slice(0, limit).trim(); +} + function extractZenOutputText(data) { if (!data || typeof data !== 'object') return null; const output = data.output; @@ -252,11 +259,12 @@ export async function summarizeText({ text, threshold = 200, maxLength = 500, ze const finalSummary = mode === 'note' ? (sanitized && sanitized !== sanitizeForNote(text) ? sanitized : distillNoteFallback(text, maxLength)) : sanitized; + const clippedSummary = clampToMaxLength(finalSummary, maxLength); return { - summary: finalSummary, + summary: clippedSummary, summarized: true, originalLength: text.length, - summaryLength: finalSummary.length, + summaryLength: clippedSummary.length, }; } diff --git a/packages/web/server/lib/text/summarization.test.js b/packages/web/server/lib/text/summarization.test.js index 889f14e4..2a37d330 100644 --- a/packages/web/server/lib/text/summarization.test.js +++ b/packages/web/server/lib/text/summarization.test.js @@ -2,9 +2,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { summarizeText } from './summarization.js'; +const originalFetch = globalThis.fetch; + +function stubFetch(fetchMock) { + globalThis.fetch = fetchMock; +} + describe('text summarization zen requests', () => { afterEach(() => { - vi.unstubAllGlobals(); + globalThis.fetch = originalFetch; }); it('uses responses endpoint for gpt models', async () => { @@ -17,7 +23,7 @@ describe('text summarization zen requests', () => { }], }), })); - vi.stubGlobal('fetch', fetchMock); + stubFetch(fetchMock); const result = await summarizeText({ text: 'Long text '.repeat(30), @@ -43,7 +49,7 @@ describe('text summarization zen requests', () => { choices: [{ message: { content: 'Chat summary' } }], }), })); - vi.stubGlobal('fetch', fetchMock); + stubFetch(fetchMock); const result = await summarizeText({ text: 'Long text '.repeat(30), @@ -61,4 +67,52 @@ describe('text summarization zen requests', () => { ); expect(result.summary).toBe('Chat summary'); }); + + it('clamps successful model summaries to the requested max length', async () => { + const fetchMock = vi.fn(async () => ({ + ok: true, + json: async () => ({ + output: [{ + type: 'message', + content: [{ type: 'output_text', text: 'This response is too long' }], + }], + }), + })); + stubFetch(fetchMock); + + const result = await summarizeText({ + text: 'Long text '.repeat(30), + threshold: 0, + maxLength: 12, + zenModel: 'gpt-5-nano', + mode: 'notification', + }); + + expect(result.summary).toBe('This respons'); + expect(result.summaryLength).toBe(12); + }); + + it('does not clamp successful model summaries for non-finite max lengths', async () => { + const fetchMock = vi.fn(async () => ({ + ok: true, + json: async () => ({ + output: [{ + type: 'message', + content: [{ type: 'output_text', text: 'Full response' }], + }], + }), + })); + stubFetch(fetchMock); + + const result = await summarizeText({ + text: 'Long text '.repeat(30), + threshold: 0, + maxLength: Infinity, + zenModel: 'gpt-5-nano', + mode: 'notification', + }); + + expect(result.summary).toBe('Full response'); + expect(result.summaryLength).toBe(13); + }); });