Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions core/test/agents/processors/code_execution_request_processor_test.ts
Original file line number Diff line number Diff line change
@@ -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<CodeExecutionResult> {
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> = {}): LlmRequest {
return {
contents: [],
toolsDict: {},
liveConnectConfig: {},
...overrides,
};
}

async function collectEvents<T>(gen: AsyncGenerator<T>): Promise<T[]> {
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);
});
});
});