diff --git a/src/__tests__/summarize.test.ts b/src/__tests__/summarize.test.ts index e4107d1..3313897 100644 --- a/src/__tests__/summarize.test.ts +++ b/src/__tests__/summarize.test.ts @@ -239,17 +239,27 @@ describe('summarizeTaskName', () => { expect(result.length).toBeLessThanOrEqual(30); }); - it('should handle mixed Japanese/English with romanization', async () => { - // When - const result = await summarizeTaskName('Add romanization', { cwd: '/project', useLLM: false }); - - // Then - expect(result).toMatch(/^[a-z0-9-]+$/); - expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens - }); - - it('should use romaji by default', async () => { - // Given: branchNameStrategy is not set (undefined) + it('should handle mixed Japanese/English with romanization', async () => { + // When + const result = await summarizeTaskName('Add romanization', { cwd: '/project', useLLM: false }); + + // Then + expect(result).toMatch(/^[a-z0-9-]+$/); + expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens + }); + + it('should handle very long names in romanization mode without stack overflow', async () => { + const result = await summarizeTaskName('a'.repeat(12000), { + cwd: '/project', + useLLM: false, + }); + + expect(result).toBe('a'.repeat(30)); + expect(mockProviderCall).not.toHaveBeenCalled(); + }); + + it('should use romaji by default', async () => { + // Given: branchNameStrategy is not set (undefined) mockResolveConfigValues.mockReturnValue({ provider: 'claude', model: undefined, diff --git a/src/infra/task/summarize.ts b/src/infra/task/summarize.ts index 662b2fb..01f53c9 100644 --- a/src/infra/task/summarize.ts +++ b/src/infra/task/summarize.ts @@ -14,12 +14,33 @@ import type { SummarizeOptions } from './types.js'; export type { SummarizeOptions }; const log = createLogger('summarize'); +const MAX_ROMAJI_CHUNK_SIZE = 1024; + +function toRomajiSafely(text: string): string { + const romajiOptions = { customRomajiMapping: {} }; + try { + if (text.length <= MAX_ROMAJI_CHUNK_SIZE) { + return wanakana.toRomaji(text, romajiOptions); + } + const convertedChunks: string[] = []; + for (let i = 0; i < text.length; i += MAX_ROMAJI_CHUNK_SIZE) { + convertedChunks.push( + wanakana.toRomaji(text.slice(i, i + MAX_ROMAJI_CHUNK_SIZE), romajiOptions), + ); + } + return convertedChunks.join(''); + } catch { + // Avoid blocking branch/task creation on rare parser edge cases or deep recursion + // with very long mixed/ASCII inputs. + return text; + } +} /** * Convert Japanese text to romaji slug. */ function toRomajiSlug(text: string): string { - const romaji = wanakana.toRomaji(text, { customRomajiMapping: {} }); + const romaji = toRomajiSafely(text); return slugify(romaji); }