| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| import { ToolOutputDistillationService } from './toolDistillationService.js'; |
| import type { Config, Part } from '../index.js'; |
| import type { GeminiClient } from '../core/client.js'; |
|
|
| vi.mock('../utils/fileUtils.js', () => ({ |
| saveTruncatedToolOutput: vi.fn().mockResolvedValue('mocked-path'), |
| })); |
|
|
| describe('ToolOutputDistillationService', () => { |
| let mockConfig: Config; |
| let mockGeminiClient: GeminiClient; |
| let service: ToolOutputDistillationService; |
|
|
| beforeEach(() => { |
| mockConfig = { |
| getToolMaxOutputTokens: vi.fn().mockReturnValue(100), |
| getToolSummarizationThresholdTokens: vi.fn().mockReturnValue(100), |
| getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), |
| storage: { |
| getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini'), |
| }, |
| telemetry: { |
| logEvent: vi.fn(), |
| }, |
| } as unknown as Config; |
| mockGeminiClient = { |
| generateContent: vi.fn().mockResolvedValue({ |
| candidates: [{ content: { parts: [{ text: 'Mock Intent Summary' }] } }], |
| }), |
| } as unknown as GeminiClient; |
| service = new ToolOutputDistillationService( |
| mockConfig, |
| mockGeminiClient, |
| 'test-prompt-id', |
| ); |
| }); |
|
|
| it('should generate a structural map for oversized content within limits', async () => { |
| |
| const largeContent = 'A'.repeat(500); |
| const result = await service.distill('test-tool', 'call-1', largeContent); |
|
|
| expect(mockGeminiClient.generateContent).toHaveBeenCalled(); |
| const text = |
| typeof result.truncatedContent === 'string' |
| ? result.truncatedContent |
| : (result.truncatedContent as Array<{ text: string }>)[0].text; |
| expect(text).toContain('Strategic Significance'); |
| }); |
|
|
| it('should structurally truncate functionResponse while preserving schema', async () => { |
| |
| const hugeValue = 'H'.repeat(1000); |
| const content = [ |
| { |
| functionResponse: { |
| name: 'test_tool', |
| id: '123', |
| response: { |
| stdout: hugeValue, |
| stderr: 'no error', |
| }, |
| }, |
| }, |
| ] as unknown as Part[]; |
|
|
| const result = await service.distill('test-tool', 'call-1', content); |
| const truncatedParts = result.truncatedContent as Part[]; |
| expect(truncatedParts.length).toBe(1); |
| const fr = truncatedParts[0].functionResponse!; |
| const resp = fr.response as Record<string, unknown>; |
| expect(fr.name).toBe('test_tool'); |
| expect(resp['stderr']).toBe('no error'); |
| expect(resp['stdout'] as string).toContain('[Message Normalized'); |
| expect(resp['stdout'] as string).toContain('Full output saved to'); |
| }); |
|
|
| it('should skip structural map for extremely large content exceeding MAX_DISTILLATION_SIZE', async () => { |
| const massiveContent = 'A'.repeat(1_000_001); |
| const result = await service.distill('test-tool', 'call-2', massiveContent); |
|
|
| expect(mockGeminiClient.generateContent).not.toHaveBeenCalled(); |
| const text = |
| typeof result.truncatedContent === 'string' |
| ? result.truncatedContent |
| : (result.truncatedContent as Array<{ text: string }>)[0].text; |
| expect(text).not.toContain('Strategic Significance'); |
| }); |
|
|
| it('should skip structural map for content below summarization threshold', async () => { |
| |
| const mediumContent = 'A'.repeat(110); |
| const result = await service.distill('test-tool', 'call-3', mediumContent); |
|
|
| expect(mockGeminiClient.generateContent).not.toHaveBeenCalled(); |
| expect(result.truncatedContent).not.toContain('Mock Intent Summary'); |
| }); |
| }); |
|
|