Skip to content

[BUG] IntegrationConnectorTool.runAsync mutates the caller-supplied args map #1278

Description

@SuperCorleone

Describe the Bug

runAsync calls args.put(...) several times on the incoming args map. In production the map originates from functionCall.args() (a Jackson-deserialized mutable LinkedHashMap), so it works today — but mutating a caller-owned argument map is fragile and inconsistent with every other tool, and throws UnsupportedOperationException if a caller passes an immutable map (Map.of(...)/ImmutableMap). The upstream Python ADK copies args rather than mutating.

Root Cause

// IntegrationConnectorTool.runAsync (L140-149)
args.put("connectionName", connectionName);
args.put("serviceName", serviceName);
... // mutates the caller's map in place

Observed Behavior

java.lang.UnsupportedOperationException
  at java.base/java.util.ImmutableCollections.uoe(...)
  at ...IntegrationConnectorTool.runAsync(IntegrationConnectorTool.java:140)

Suggested Fix

Work on a copy: Map<String,Object> a = new LinkedHashMap<>(args); a.put(...); and use a.

Corresponding Test (generated)

@Test
public void testRunAsyncWithConnectionNameAndOperation() throws Exception {
    // Arrange - Create a tool that has operation set from the OpenAPI spec parsing
    String openApiSpecWithOperation = "{ \"openApiSpec\": \"{\\\"paths\\\":{\\\"/test\\\":{\\\"get\\\":{\\\"operationId\\\":\\\"testOperation\\\",\\\"x-operation\\\":\\\"testOperation\\\"}}}}\" }";

    IntegrationConnectorTool toolWithOperation = new IntegrationConnectorTool(
        openApiSpecWithOperation,
        PATH_URL,
        TOOL_NAME,
        TOOL_DESCRIPTION,
        "connection1",
        "service1",
        "host1",
        SERVICE_ACCOUNT_JSON,
        httpClient,
        credentialsHelper
    );

    Map<String, Object> args = ImmutableMap.of("param1", "value1");
    when(credentialsHelper.getGoogleCredentials(anyString())).thenReturn(mock(Credentials.class));

    HttpResponse<String> mockResponse = mock(HttpResponse.class);
    when(mockResponse.statusCode()).thenReturn(200);
    when(mockResponse.body()).thenReturn("{\"result\":\"success\"}");
    when(httpClient.send(any(HttpRequest.class), any())).thenAnswer(invocation -> {
        return mockResponse;
    });

    // Act
    Single<Map<String, Object>> result = toolWithOperation.runAsync(args, toolContext);
    Map<String, Object> actualResult = result.blockingGet();

    // Assert
    assertNotNull(actualResult);
    assertEquals("{\"result\":\"success\"}", actualResult.get("result"));
    assertTrue(actualResult.containsKey("result"));
    assertFalse(actualResult.containsKey("error"));

    // Verify args were modified correctly
    assertTrue(args.containsKey("connectionName"));
    assertTrue(args.containsKey("serviceName"));
    assertTrue(args.containsKey("host"));
    assertTrue(args.containsKey("operation"));
}

Note: the test constructs args with ImmutableMap.of(...) yet asserts (at the end) that args was mutated — so it fails with UnsupportedOperationException at the first args.put(...) inside runAsync, before reaching those assertions. This both exposes the production smell (mutating the caller's map) and is itself an inconsistent test.

Environment

  • Project: adk / google-adk (com.google.adk) · upstream: github.com/google/adk-java
  • Version: 0.5.1-SNAPSHOT (local) · Commit: <fill SUT commit> · Java 17 · OS macOS

This input was generated by the test case generator TestFusion developed in our STAR lab.

Metadata

Metadata

Assignees

Labels

waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions