diff --git a/core/test/agents/processors/code_execution_request_processor_test.ts b/core/test/agents/processors/code_execution_request_processor_test.ts new file mode 100644 index 00000000..cf67f308 --- /dev/null +++ b/core/test/agents/processors/code_execution_request_processor_test.ts @@ -0,0 +1,221 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + BaseAgent, + InvocationContext, + LlmAgent, + LlmRequest, + PluginManager, + createSession, +} from '@google/adk'; +import {describe, expect, it} from 'vitest'; +import { + CODE_EXECUTION_REQUEST_PROCESSOR, + CodeExecutionResponseProcessor, +} from '../../../src/agents/processors/code_execution_request_processor.js'; +import { + BaseCodeExecutor, + ExecuteCodeParams, +} from '../../../src/code_executors/base_code_executor.js'; +import {CodeExecutionResult} from '../../../src/code_executors/code_execution_utils.js'; + +class MockBaseAgent extends BaseAgent { + constructor(name: string) { + super({name}); + } + protected async *runAsyncImpl(_context: InvocationContext) {} + protected async *runLiveImpl(_context: InvocationContext) {} +} + +class TestCodeExecutor extends BaseCodeExecutor { + async executeCode(_params: ExecuteCodeParams): Promise { + return {stdout: '', stderr: '', outputFiles: []}; + } +} + +function createMockInvocationContext(agent: BaseAgent): InvocationContext { + return new InvocationContext({ + invocationId: 'test-invocation', + agent, + session: createSession({ + id: 'test-session', + events: [], + appName: 'test-app', + userId: 'test-user', + }), + pluginManager: new PluginManager([]), + }); +} + +function createLlmRequest(overrides: Partial = {}): LlmRequest { + return { + contents: [], + toolsDict: {}, + liveConnectConfig: {}, + ...overrides, + }; +} + +async function collectEvents(gen: AsyncGenerator): Promise { + const results: T[] = []; + for await (const event of gen) { + results.push(event); + } + return results; +} + +describe('CodeExecutionRequestProcessor', () => { + describe('early-exit paths', () => { + it('yields no events and leaves request unchanged for a non-LlmAgent', async () => { + const agent = new MockBaseAgent('non-llm-agent'); + const ctx = createMockInvocationContext(agent); + const llmRequest = createLlmRequest({ + contents: [{role: 'user', parts: [{text: 'hello'}]}], + }); + + const events = await collectEvents( + CODE_EXECUTION_REQUEST_PROCESSOR.runAsync(ctx, llmRequest), + ); + + expect(events).toHaveLength(0); + expect(llmRequest.contents).toHaveLength(1); + }); + + it('yields no events when LlmAgent has no codeExecutor', async () => { + const agent = new LlmAgent({ + name: 'agent-no-executor', + model: 'gemini-2.5-flash', + }); + const ctx = createMockInvocationContext(agent); + const llmRequest = createLlmRequest({ + contents: [{role: 'user', parts: [{text: 'hello'}]}], + }); + + const events = await collectEvents( + CODE_EXECUTION_REQUEST_PROCESSOR.runAsync(ctx, llmRequest), + ); + + expect(events).toHaveLength(0); + }); + + it('calls runPreProcessor and proceeds to convertCodeExecutionParts when codeExecutor is BaseCodeExecutor', async () => { + const executor = new TestCodeExecutor(); + const agent = new LlmAgent({ + name: 'agent-with-executor', + model: 'gemini-2.5-flash', + codeExecutor: executor, + }); + const ctx = createMockInvocationContext(agent); + const llmRequest = createLlmRequest({ + contents: [{role: 'user', parts: [{text: 'hello'}]}], + }); + + // Should not throw — runPreProcessor exits early because + // isBuiltInCodeExecutor is false and optimizeDataFile is false + const events = await collectEvents( + CODE_EXECUTION_REQUEST_PROCESSOR.runAsync(ctx, llmRequest), + ); + + expect(events).toHaveLength(0); + // Content should still be present after processing + expect(llmRequest.contents).toHaveLength(1); + }); + }); +}); + +describe('CodeExecutionResponseProcessor', () => { + const responseProcessor = new CodeExecutionResponseProcessor(); + + describe('early-exit paths', () => { + it('yields no events for a partial response', async () => { + const agent = new LlmAgent({ + name: 'agent', + model: 'gemini-2.5-flash', + codeExecutor: new TestCodeExecutor(), + }); + const ctx = createMockInvocationContext(agent); + const partialResponse = { + partial: true, + content: {role: 'model', parts: [{text: 'thinking...'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, partialResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events for a non-LlmAgent', async () => { + const agent = new MockBaseAgent('non-llm'); + const ctx = createMockInvocationContext(agent); + const llmResponse = { + partial: false, + content: {role: 'model', parts: [{text: 'done'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events when LlmAgent has no codeExecutor', async () => { + const agent = new LlmAgent({ + name: 'agent-no-executor', + model: 'gemini-2.5-flash', + }); + const ctx = createMockInvocationContext(agent); + const llmResponse = { + partial: false, + content: {role: 'model', parts: [{text: 'done'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events when response has no content', async () => { + const agent = new LlmAgent({ + name: 'agent-with-executor', + model: 'gemini-2.5-flash', + codeExecutor: new TestCodeExecutor(), + }); + const ctx = createMockInvocationContext(agent); + const llmResponse = {partial: false}; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + + it('yields no events when response content has no code block', async () => { + const agent = new LlmAgent({ + name: 'agent-with-executor', + model: 'gemini-2.5-flash', + codeExecutor: new TestCodeExecutor(), + }); + const ctx = createMockInvocationContext(agent); + const llmResponse = { + partial: false, + content: {role: 'model', parts: [{text: 'plain text response'}]}, + }; + + const events = await collectEvents( + responseProcessor.runAsync(ctx, llmResponse), + ); + + expect(events).toHaveLength(0); + }); + }); +});