/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ 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 () => { // > threshold * SUMMARIZATION_THRESHOLD (100 * 4 = 400) 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 () => { // threshold is 100 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; 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); // > MAX_DISTILLATION_SIZE 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 () => { // > threshold but < threshold * SUMMARIZATION_THRESHOLD 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'); }); });