Download packages/core/src/voice/responseFormatter.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 9.94 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/voice/responseFormatter.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/voice/responseFormatter.test.ts
-
curl -L -o responseFormatter.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/voice/responseFormatter.test.ts
9.94 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect } from 'vitest'; | |
| import { formatForSpeech } from './responseFormatter.js'; | |
| describe('formatForSpeech', () => { | |
| describe('edge cases', () => { | |
| it('should return empty string for empty input', () => { | |
| expect(formatForSpeech('')).toBe(''); | |
| }); | |
| it('should return plain text unchanged', () => { | |
| expect(formatForSpeech('Hello world')).toBe('Hello world'); | |
| }); | |
| }); | |
| describe('ANSI escape codes', () => { | |
| it('should strip color codes', () => { | |
| expect(formatForSpeech('\x1b[31mError\x1b[0m')).toBe('Error'); | |
| }); | |
| it('should strip bold/dim codes', () => { | |
| expect(formatForSpeech('\x1b[1mBold\x1b[22m text')).toBe('Bold text'); | |
| }); | |
| it('should strip cursor movement codes', () => { | |
| expect(formatForSpeech('line1\x1b[2Kline2')).toBe('line1line2'); | |
| }); | |
| }); | |
| describe('markdown stripping', () => { | |
| it('should strip bold markers **text**', () => { | |
| expect(formatForSpeech('**Error**: something went wrong')).toBe( | |
| 'Error: something went wrong', | |
| ); | |
| }); | |
| it('should strip bold markers __text__', () => { | |
| expect(formatForSpeech('__Error__: something')).toBe('Error: something'); | |
| }); | |
| it('should strip italic markers *text*', () => { | |
| expect(formatForSpeech('*note*: pay attention')).toBe( | |
| 'note: pay attention', | |
| ); | |
| }); | |
| it('should strip inline code backticks', () => { | |
| expect(formatForSpeech('Run `npm install` first')).toBe( | |
| 'Run npm install first', | |
| ); | |
| }); | |
| it('should strip blockquote prefix', () => { | |
| expect(formatForSpeech('> This is a quote')).toBe('This is a quote'); | |
| }); | |
| it('should strip heading markers', () => { | |
| expect(formatForSpeech('# Results\n## Details')).toBe('Results\nDetails'); | |
| }); | |
| it('should replace markdown links with link text', () => { | |
| expect(formatForSpeech('[Gemini API](https://ai.google.dev)')).toBe( | |
| 'Gemini API', | |
| ); | |
| }); | |
| it('should strip unordered list markers', () => { | |
| expect(formatForSpeech('- item one\n- item two')).toBe( | |
| 'item one\nitem two', | |
| ); | |
| }); | |
| it('should strip ordered list markers', () => { | |
| expect(formatForSpeech('1. first\n2. second')).toBe('first\nsecond'); | |
| }); | |
| }); | |
| describe('fenced code blocks', () => { | |
| it('should unwrap a plain code block', () => { | |
| expect(formatForSpeech('```\nconsole.log("hi")\n```')).toBe( | |
| 'console.log("hi")', | |
| ); | |
| }); | |
| it('should unwrap a language-tagged code block', () => { | |
| expect(formatForSpeech('```typescript\nconst x = 1;\n```')).toBe( | |
| 'const x = 1;', | |
| ); | |
| }); | |
| it('should summarise a JSON object code block above threshold', () => { | |
| const json = JSON.stringify({ status: 'ok', count: 42, items: [] }); | |
| // Pass jsonThreshold lower than the json string length (38 chars) | |
| const result = formatForSpeech(`\`\`\`json\n${json}\n\`\`\``, { | |
| jsonThreshold: 10, | |
| }); | |
| expect(result).toBe('(JSON object with 3 keys)'); | |
| }); | |
| it('should summarise a JSON array code block above threshold', () => { | |
| const json = JSON.stringify([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); | |
| // Pass jsonThreshold lower than the json string length (23 chars) | |
| const result = formatForSpeech(`\`\`\`\n${json}\n\`\`\``, { | |
| jsonThreshold: 10, | |
| }); | |
| expect(result).toBe('(JSON array with 10 items)'); | |
| }); | |
| it('should summarise a large JSON object using default threshold', () => { | |
| // Build a JSON object whose stringified form exceeds the default 80-char threshold | |
| const big = { | |
| status: 'success', | |
| count: 42, | |
| items: ['alpha', 'beta', 'gamma'], | |
| meta: { page: 1, totalPages: 10 }, | |
| timestamp: '2026-03-03T00:00:00Z', | |
| }; | |
| const json = JSON.stringify(big); | |
| expect(json.length).toBeGreaterThan(80); | |
| const result = formatForSpeech(`\`\`\`json\n${json}\n\`\`\``); | |
| expect(result).toBe('(JSON object with 5 keys)'); | |
| }); | |
| it('should not summarise a tiny JSON value', () => { | |
| // Below the default 80-char threshold → keep as-is | |
| const result = formatForSpeech('```json\n{"a":1}\n```', { | |
| jsonThreshold: 80, | |
| }); | |
| expect(result).toBe('{"a":1}'); | |
| }); | |
| }); | |
| describe('path abbreviation', () => { | |
| it('should abbreviate a deep Unix path (default depth 3)', () => { | |
| const result = formatForSpeech( | |
| 'at /home/user/project/packages/core/src/tools/file.ts', | |
| ); | |
| expect(result).toContain('\u2026/src/tools/file.ts'); | |
| expect(result).not.toContain('/home/user/project'); | |
| }); | |
| it('should convert :line suffix to "line N"', () => { | |
| const result = formatForSpeech( | |
| 'Error at /home/user/project/src/tools/file.ts:142', | |
| ); | |
| expect(result).toContain('line 142'); | |
| }); | |
| it('should drop column from :line:col suffix', () => { | |
| const result = formatForSpeech( | |
| 'Error at /home/user/project/src/tools/file.ts:142:7', | |
| ); | |
| expect(result).toContain('line 142'); | |
| expect(result).not.toContain(':7'); | |
| }); | |
| it('should respect custom pathDepth option', () => { | |
| const result = formatForSpeech( | |
| '/home/user/project/packages/core/src/file.ts', | |
| { pathDepth: 2 }, | |
| ); | |
| expect(result).toContain('\u2026/src/file.ts'); | |
| }); | |
| it('should not abbreviate a short path within depth', () => { | |
| const result = formatForSpeech('/src/file.ts', { pathDepth: 3 }); | |
| // Only 2 segments — no abbreviation needed | |
| expect(result).toBe('/src/file.ts'); | |
| }); | |
| it('should abbreviate a Windows path on a non-C drive', () => { | |
| const result = formatForSpeech( | |
| 'D:\\Users\\project\\packages\\core\\src\\file.ts', | |
| { pathDepth: 3 }, | |
| ); | |
| expect(result).toContain('\u2026/core/src/file.ts'); | |
| expect(result).not.toContain('D:\\Users\\project'); | |
| }); | |
| it('should convert :line on a Windows path on a non-C drive', () => { | |
| const result = formatForSpeech( | |
| 'Error at D:\\Users\\project\\src\\tools\\file.ts:55', | |
| ); | |
| expect(result).toContain('line 55'); | |
| expect(result).not.toContain('D:\\Users\\project'); | |
| }); | |
| it('should abbreviate a Unix path containing a scoped npm package segment', () => { | |
| const result = formatForSpeech( | |
| 'at /home/user/project/node_modules/@google/gemini-cli-core/src/index.ts:12:3', | |
| { pathDepth: 5 }, | |
| ); | |
| expect(result).toContain('line 12'); | |
| expect(result).not.toContain(':3'); | |
| expect(result).toContain('@google'); | |
| }); | |
| }); | |
| describe('stack trace collapsing', () => { | |
| it('should collapse a multi-frame stack trace', () => { | |
| const trace = [ | |
| 'Error: ENOENT', | |
| ' at Object.open (/project/src/file.ts:10:5)', | |
| ' at Module._load (/project/node_modules/loader.js:20:3)', | |
| ' at Function.Module._load (/project/node_modules/loader.js:30:3)', | |
| ].join('\n'); | |
| const result = formatForSpeech(trace); | |
| expect(result).toContain('and 2 more frames'); | |
| expect(result).not.toContain('Module._load'); | |
| }); | |
| it('should not collapse a single stack frame', () => { | |
| const trace = | |
| 'Error: ENOENT\n at Object.open (/project/src/file.ts:10:5)'; | |
| const result = formatForSpeech(trace); | |
| expect(result).not.toContain('more frames'); | |
| }); | |
| it('should preserve surrounding text when collapsing a stack trace', () => { | |
| const input = [ | |
| 'Operation failed.', | |
| ' at Object.open (/project/src/file.ts:10:5)', | |
| ' at Module._load (/project/node_modules/loader.js:20:3)', | |
| ' at Function.load (/project/node_modules/loader.js:30:3)', | |
| 'Please try again.', | |
| ].join('\n'); | |
| const result = formatForSpeech(input); | |
| expect(result).toContain('Operation failed.'); | |
| expect(result).toContain('Please try again.'); | |
| expect(result).toContain('and 2 more frames'); | |
| }); | |
| }); | |
| describe('truncation', () => { | |
| it('should truncate output longer than maxLength', () => { | |
| const long = 'word '.repeat(200); | |
| const result = formatForSpeech(long, { maxLength: 50 }); | |
| expect(result.length).toBeLessThanOrEqual( | |
| 50 + '\u2026 (1000 chars total)'.length, | |
| ); | |
| expect(result).toContain('\u2026'); | |
| expect(result).toContain('chars total'); | |
| }); | |
| it('should not truncate output within maxLength', () => { | |
| const short = 'Hello world'; | |
| expect(formatForSpeech(short, { maxLength: 500 })).toBe('Hello world'); | |
| }); | |
| }); | |
| describe('whitespace normalisation', () => { | |
| it('should collapse more than two consecutive blank lines', () => { | |
| const result = formatForSpeech('para1\n\n\n\n\npara2'); | |
| expect(result).toBe('para1\n\npara2'); | |
| }); | |
| it('should trim leading and trailing whitespace', () => { | |
| expect(formatForSpeech(' hello ')).toBe('hello'); | |
| }); | |
| }); | |
| describe('real-world examples', () => { | |
| it('should clean an ENOENT error with markdown and path', () => { | |
| const input = | |
| '**Error**: `ENOENT: no such file or directory`\n> at /home/user/project/packages/core/src/tools/file-utils.ts:142:7'; | |
| const result = formatForSpeech(input); | |
| expect(result).not.toContain('**'); | |
| expect(result).not.toContain('`'); | |
| expect(result).not.toContain('>'); | |
| expect(result).toContain('Error'); | |
| expect(result).toContain('ENOENT'); | |
| expect(result).toContain('line 142'); | |
| }); | |
| it('should clean a heading + list response', () => { | |
| const input = '# Results\n- item one\n- item two\n- item three'; | |
| const result = formatForSpeech(input); | |
| expect(result).toBe('Results\nitem one\nitem two\nitem three'); | |
| }); | |
| }); | |
| }); | |