feat: add ITK integration for cross-SDK interoperability testing#917
feat: add ITK integration for cross-SDK interoperability testing#917kabir wants to merge 2 commits into
Conversation
Add a new `itk/` Maven module that builds a Quarkus-based ITK agent from the current SDK source. This agent processes protobuf-encoded instructions to forward messages to other agents via JSONRPC, gRPC, and REST transports, enabling cross-SDK interoperability testing with the A2A Integration Test Kit. Includes: - ITK agent (AgentExecutorProducer, AgentCardProducer, AdditionalRoutes) - PR and nightly test scenarios (scenarios.json, scenarios_full.json) - Runner script (run_itk.sh) for Docker-based ITK execution - GitHub Actions workflows for PR validation and nightly runs - BOM verifier exclusions for the non-library ITK module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the Integration Test Kit (ITK) agent module (itk) to support cross-SDK interoperability testing, including runner scripts, test scenarios, and agent execution logic. Several critical issues were identified in the review: reading the request body in an early Vert.x handler will prevent subsequent handlers from processing POST requests; attempting to Base64-decode raw protobuf bytes will cause exceptions; catching InterruptedException silently during task holds leaves the emitter in an incomplete state; and a potential NullPointerException exists when handling ExecutionException causes. Additionally, the cleanup process in run_itk.sh should be made safer, and a bug in the inline Python script's exception handler needs to be fixed to avoid masking JSON parsing errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| router.route().order(Integer.MIN_VALUE) | ||
| .handler(ctx -> { | ||
| String method = ctx.request().method().name(); | ||
| String path = ctx.request().path(); | ||
| String contentType = ctx.request().getHeader("Content-Type"); | ||
| log.info("Incoming {} {} Content-Type={}", method, path, contentType); | ||
| if ("POST".equals(method)) { | ||
| ctx.request().body().onSuccess(buffer -> { | ||
| if (buffer != null) { | ||
| log.debug("POST {} payload: {}", path, buffer.toString()); | ||
| } | ||
| }); | ||
| } | ||
| ctx.next(); | ||
| }); |
There was a problem hiding this comment.
Calling ctx.request().body() in an early handler (with order(Integer.MIN_VALUE)) initiates reading of the request body stream. In Vert.x, once the request stream is consumed, subsequent handlers (such as JAX-RS/RESTEasy or other route handlers) will not be able to read it again, causing all incoming POST requests (JSONRPC, REST) to fail or receive empty bodies. To prevent this, avoid reading the body stream in this early logging handler.
router.route().order(Integer.MIN_VALUE)
.handler(ctx -> {
String method = ctx.request().method().name();
String path = ctx.request().path();
String contentType = ctx.request().getHeader("Content-Type");
log.info("Incoming {} {} Content-Type={}", method, path, contentType);
ctx.next();
});| if (file instanceof FileWithBytes fwb) { | ||
| byte[] raw = Base64.getDecoder().decode(fwb.bytes()); | ||
| return Instruction.parseFrom(raw); | ||
| } |
There was a problem hiding this comment.
fwb.bytes() returns the raw binary payload of the protobuf file. Attempting to decode it as Base64 will throw an IllegalArgumentException because raw protobuf bytes are not Base64-encoded. You should parse the instruction directly from the raw bytes.
if (file instanceof FileWithBytes fwb) {
return Instruction.parseFrom(fwb.bytes());
}| try { | ||
| Thread.sleep(HOLD_INTERVAL_MS); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| log.info("Task {} interrupted during hold", emitter.getTaskId()); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Catching InterruptedException inside the loop and returning silently leaves the emitter in the TASK_STATE_WORKING state indefinitely, as neither complete() nor fail() is called. Since the outer try-catch block already catches InterruptedException and correctly fails the emitter, you can simplify this by letting Thread.sleep throw the exception directly.
Thread.sleep(HOLD_INTERVAL_MS);| } catch (ExecutionException e) { | ||
| log.error("Remote agent call failed for task {}: {}", | ||
| emitter.getTaskId(), e.getCause().getMessage(), e.getCause()); | ||
| emitter.fail(); |
There was a problem hiding this comment.
e.getCause() can potentially be null for an ExecutionException. Calling e.getCause().getMessage() without a null check will throw a NullPointerException, masking the original failure. Use defensive programming to handle a null cause safely.
} catch (ExecutionException e) {
Throwable cause = e.getCause();
String errorMsg = cause != null ? cause.getMessage() : e.getMessage();
log.error("Remote agent call failed for task {}: {}",
emitter.getTaskId(), errorMsg, cause);
emitter.fail();| #!/bin/bash | ||
| set -ex | ||
|
|
||
| # Set default log level | ||
| export ITK_LOG_LEVEL="${ITK_LOG_LEVEL:-INFO}" | ||
|
|
||
| # Initialize default exit code | ||
| RESULT=1 | ||
|
|
||
| # Cleanup function to be called on exit | ||
| cleanup() { | ||
| set +x | ||
| echo "Cleaning up artifacts..." | ||
| docker stop itk-service > /dev/null 2>&1 || true | ||
| docker rm itk-service > /dev/null 2>&1 || true | ||
| docker rmi itk_service > /dev/null 2>&1 || true | ||
| rm -rf a2a-itk > /dev/null 2>&1 || true | ||
| echo "Done. Final exit code: $RESULT" | ||
| } |
There was a problem hiding this comment.
If the script fails while inside the a2a-itk directory, the cleanup trap will execute rm -rf a2a-itk from within that directory. Deleting the current working directory can fail or cause shell issues on some systems. It is safer to save the original directory at the start and cd back to it during cleanup.
#!/bin/bash
set -ex
# Save original directory to ensure cleanup runs from a safe location
ORIGINAL_DIR=$(pwd)
# Set default log level
export ITK_LOG_LEVEL="${ITK_LOG_LEVEL:-INFO}"
# Initialize default exit code
RESULT=1
# Cleanup function to be called on exit
cleanup() {
set +x
echo "Cleaning up artifacts..."
cd "$ORIGINAL_DIR" > /dev/null 2>&1 || true
docker stop itk-service > /dev/null 2>&1 || true
docker rm itk-service > /dev/null 2>&1 || true
docker rmi itk_service > /dev/null 2>&1 || true
rm -rf a2a-itk > /dev/null 2>&1 || true
echo "Done. Final exit code: $RESULT"
}| echo "$RESPONSE" | python3 -c " | ||
| import sys, json | ||
| try: | ||
| data = json.load(sys.stdin) | ||
| all_passed = data.get('all_passed', False) | ||
| results = data.get('results', {}) | ||
| for test, passed in results.items(): | ||
| status = 'PASSED' if passed else 'FAILED' | ||
| print(f'{test}: {status}') | ||
| print('--------------------------------------------------------') | ||
| print(f'OVERALL STATUS: {\"PASSED\" if all_passed else \"FAILED\"}') | ||
| if not all_passed: | ||
| sys.exit(1) | ||
| except Exception as e: | ||
| print(f'Error parsing results: {e}') | ||
| print(f'Raw response: {data if \"data\" in locals() else \"no data\"}') | ||
| sys.exit(1) | ||
| " |
There was a problem hiding this comment.
If json.load(sys.stdin) fails due to an invalid JSON response, the except block tries to print data, which is undefined, resulting in a secondary exception and hiding the actual raw response. Reading the raw stdin first allows you to print the exact invalid response for easier debugging.
| echo "$RESPONSE" | python3 -c " | |
| import sys, json | |
| try: | |
| data = json.load(sys.stdin) | |
| all_passed = data.get('all_passed', False) | |
| results = data.get('results', {}) | |
| for test, passed in results.items(): | |
| status = 'PASSED' if passed else 'FAILED' | |
| print(f'{test}: {status}') | |
| print('--------------------------------------------------------') | |
| print(f'OVERALL STATUS: {\"PASSED\" if all_passed else \"FAILED\"}') | |
| if not all_passed: | |
| sys.exit(1) | |
| except Exception as e: | |
| print(f'Error parsing results: {e}') | |
| print(f'Raw response: {data if \"data\" in locals() else \"no data\"}') | |
| sys.exit(1) | |
| " | |
| echo "$RESPONSE" | python3 -c " | |
| import sys, json | |
| raw = sys.stdin.read() | |
| try: | |
| data = json.loads(raw) | |
| all_passed = data.get('all_passed', False) | |
| results = data.get('results', {}) | |
| for test, passed in results.items(): | |
| status = 'PASSED' if passed else 'FAILED' | |
| print(f'{test}: {status}') | |
| print('--------------------------------------------------------') | |
| print(f'OVERALL STATUS: {\\\"PASSED\\\" if all_passed else \\\"FAILED\\\"}') | |
| if not all_passed: | |
| sys.exit(1) | |
| except Exception as e: | |
| print(f'Error parsing results: {e}') | |
| print(f'Raw response: {raw}') | |
| sys.exit(1) | |
| " |
- Add Main.java that launches quarkus-run.jar as a subprocess, bridging the exec:java entry point that current.py expects - Remove reference-bom import from itk/pom.xml — parent POM already manages all SDK artifact versions, and the BOM isn't installed to .m2 by `mvn -pl itk -am install`, breaking standalone resolution - Support podman as alternative container runtime in run_itk.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new
itk/Maven module that builds a Quarkus-based ITK agent from the current SDK source. This agent processes protobuf-encoded instructions to forward messages to other agents via JSONRPC, gRPC, and REST transports, enabling cross-SDK interoperability testing with the A2A Integration Test Kit.Includes: