Skip to content
Closed
Show file tree
Hide file tree
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
111 changes: 107 additions & 4 deletions .github/workflows/run-tck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ on:
env:
# Tag/branch of the TCK
TCK_VERSION: 1.0-dev
TCK_VERSION_0_3: 0.3.0.beta4
# Tells uv to not need a venv, and instead use system
UV_SYSTEM_PYTHON: 1
SUT_URL: http://localhost:9999
# Slow system on CI
TCK_STREAMING_TIMEOUT_0_3: 5.0

# Only run the latest job
concurrency:
Expand All @@ -27,6 +30,7 @@ jobs:
strategy:
matrix:
java-version: [17]
profile: ['', 'multi-mode']
steps:
- name: Checkout a2a-java
uses: actions/checkout@v6
Expand Down Expand Up @@ -54,7 +58,7 @@ jobs:
uv pip install -e .
working-directory: a2a-tck
- name: Start SUT
run: mvn -B quarkus:dev -Dquarkus.console.enabled=false &
run: mvn -B quarkus:dev ${{ matrix.profile && format('-P{0}', matrix.profile) || '' }} -Dquarkus.console.enabled=false &
working-directory: tck
- name: Wait for SUT to start
run: |
Expand Down Expand Up @@ -97,7 +101,7 @@ jobs:
# Extract everything after the first ═══ separator line
SUMMARY=$(sed -n '/^═══/,$p' a2a-tck/tck-output.log)
if [ -n "$SUMMARY" ]; then
echo '### TCK Results (Java ${{ matrix.java-version }})' >> $GITHUB_STEP_SUMMARY
echo '### TCK 1.0 Results (Java ${{ matrix.java-version }}${{ matrix.profile && format(', {0}', matrix.profile) || '' }})' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
Expand All @@ -112,7 +116,106 @@ jobs:
if: always()
uses: actions/upload-artifact@v7
with:
name: tck-reports-java-${{ matrix.java-version }}
name: tck-reports-java-${{ matrix.java-version }}${{ matrix.profile && format('-{0}', matrix.profile) || '' }}
path: a2a-tck/reports/
retention-days: 14
if-no-files-found: warn
if-no-files-found: warn

tck-test-0-3:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [17]
profile: ['', 'multi-mode']
steps:
- name: Checkout a2a-java
uses: actions/checkout@v6
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v5
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: maven
- name: Build a2a-java SDK
run: mvn -B install -DskipTests
- name: Checkout a2a-tck
uses: actions/checkout@v6
with:
repository: a2aproject/a2a-tck
path: a2a-tck
ref: ${{ env.TCK_VERSION_0_3 }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version-file: "a2a-tck/pyproject.toml"
- name: Install uv and Python dependencies
run: |
pip install uv
uv pip install -e .
working-directory: a2a-tck
- name: Start SUT
run: mvn -B quarkus:dev ${{ matrix.profile && format('-P{0}', matrix.profile) || '' }} -Dquarkus.console.enabled=false &
working-directory: compat-0.3/tck
- name: Wait for SUT to start
run: |
URL="${{ env.SUT_URL }}/.well-known/agent-card.json"
EXPECTED_STATUS=200
TIMEOUT=120
RETRY_INTERVAL=2
START_TIME=$(date +%s)

while true; do
CURRENT_TIME=$(date +%s)
ELAPSED_TIME=$((CURRENT_TIME - START_TIME))

if [ "$ELAPSED_TIME" -ge "$TIMEOUT" ]; then
echo "Timeout: Server did not respond with status $EXPECTED_STATUS within $TIMEOUT seconds."
exit 1
fi

HTTP_STATUS=$(curl --output /dev/null --silent --write-out "%{http_code}" "$URL") || true

if [ "$HTTP_STATUS" -eq "$EXPECTED_STATUS" ]; then
echo "Server is up! Received status $HTTP_STATUS after $ELAPSED_TIME seconds."
break;
fi

echo "Server not ready (status: $HTTP_STATUS). Retrying in $RETRY_INTERVAL seconds..."
sleep "$RETRY_INTERVAL"
done
- name: Run TCK
id: run-tck
timeout-minutes: 5
env:
TCK_STREAMING_TIMEOUT: ${{ env.TCK_STREAMING_TIMEOUT_0_3 }}
run: |
set -o pipefail
./run_tck.py --sut-url ${{ env.SUT_URL }} --category all --transports jsonrpc,grpc,rest --compliance-report report.json 2>&1 | tee tck-output.log
working-directory: a2a-tck
- name: TCK Summary
if: always() && steps.run-tck.outcome != 'skipped'
run: |
if [ -f a2a-tck/tck-output.log ]; then
SUMMARY=$(sed -n '/^═══/,$p' a2a-tck/tck-output.log)
if [ -n "$SUMMARY" ]; then
echo '### TCK 0.3 Results (Java ${{ matrix.java-version }}${{ matrix.profile && format(', {0}', matrix.profile) || '' }})' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Stop SUT
if: always()
run: |
pkill -f "quarkus:dev" || true
sleep 2
- name: Upload TCK Reports
if: always()
uses: actions/upload-artifact@v7
with:
name: tck-0.3-reports-java-${{ matrix.java-version }}${{ matrix.profile && format('-{0}', matrix.profile) || '' }}
path: |
a2a-tck/reports/
a2a-tck/report.json
retention-days: 14
if-no-files-found: warn
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
Expand Down Expand Up @@ -93,6 +94,38 @@ public void testGetAgentCardJsonDecodeError() throws Exception {
}


@Test
public void testParseV1AgentCardWithUrlAndPreferredTransport() throws Exception {
TestHttpClient client = new TestHttpClient();
client.body = JsonMessages_v0_3.V1_AGENT_CARD;

A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/");
AgentCard_v0_3 card = resolver.getAgentCard();

assertEquals("Test Agent", card.name());
assertEquals("A test agent", card.description());
assertEquals("1.0.0", card.version());
assertEquals("https://agent.example.com/a2a", card.url());
assertEquals("JSONRPC", card.preferredTransport());
assertTrue(card.capabilities().streaming());
}

@Test
public void testParseV1AgentCardDefaultsPreferredTransportWhenAbsent() throws Exception {
TestHttpClient client = new TestHttpClient();
client.body = JsonMessages_v0_3.V1_AGENT_CARD_NO_PREFERRED_TRANSPORT;

A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/");
AgentCard_v0_3 card = resolver.getAgentCard();

assertEquals("Minimal Agent", card.name());
assertEquals("https://agent.example.com/a2a", card.url());
// v0.3 compact constructor defaults null preferredTransport to "JSONRPC"
assertEquals("JSONRPC", card.preferredTransport());
// v1.0-only fields such as supportedInterfaces are unknown to v0.3 and must be ignored
assertNull(card.additionalInterfaces());
}

@Test
public void testGetAgentCardRequestError() throws Exception {
TestHttpClient client = new TestHttpClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ public class JsonMessages_v0_3 {
]
}""";

/**
* A v1.0 format agent card returned by a current server. Contains the new {@code supportedInterfaces}
* and {@code securityRequirements} fields (v1.0-only), plus the backward-compat {@code url} and
* {@code preferredTransport} fields that the v0.3 client relies on.
*/
static final String V1_AGENT_CARD = """
{
"name": "Test Agent",
"description": "A test agent",
"url": "https://agent.example.com/a2a",
"preferredTransport": "JSONRPC",
"version": "1.0.0",
"supportedInterfaces": [
{"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"},
{"protocolBinding": "GRPC", "url": "grpc://agent.example.com:9090"}
],
"capabilities": {
"streaming": true,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [],
"securityRequirements": [{"oauth2": ["read"]}]
}""";

/**
* A v1.0 format agent card without an explicit {@code preferredTransport}. The v0.3 client must
* fall back to the default transport ("JSONRPC").
*/
static final String V1_AGENT_CARD_NO_PREFERRED_TRANSPORT = """
{
"name": "Minimal Agent",
"description": "Minimal agent card",
"url": "https://agent.example.com/a2a",
"version": "2.0.0",
"supportedInterfaces": [
{"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"}
],
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": []
}""";

static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """
{
"name": "GeoSpatial Route Planner Agent Extended",
Expand Down
42 changes: 42 additions & 0 deletions compat-0.3/tck/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,48 @@
</dependency>
</dependencies>

<profiles>
<profile>
<id>multi-mode</id>
<dependencies>
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-jsonrpc</artifactId>
</dependency>
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-grpc</artifactId>
</dependency>
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-rest</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-multi-mode-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/multi-mode/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

<build>
<plugins>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.a2aproject.sdk.compat03.tck.server;

import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

import org.a2aproject.sdk.server.PublicAgentCard;
import org.a2aproject.sdk.spec.AgentCapabilities;
import org.a2aproject.sdk.spec.AgentCard;
import org.a2aproject.sdk.spec.AgentInterface;
import org.a2aproject.sdk.spec.AgentSkill;
import org.a2aproject.sdk.spec.TransportProtocol;

/**
* Stub producer that overrides the v1.0 DefaultProducers @DefaultBean when the
* multi-mode profile adds v1.0 reference dependencies to the classpath.
* This will be replaced by a proper translation layer in the future.
*/
@ApplicationScoped
public class StubAgentCardProducer {

private static final String DEFAULT_SUT_URL = "http://localhost:9999";

@Produces
@PublicAgentCard
public AgentCard createStubAgentCard() {
return AgentCard.builder()
.name("stub")
.description("Stub agent card for multi-mode testing")
.version("0.0.0")
.capabilities(AgentCapabilities.builder().build())
.defaultInputModes(List.of("text"))
.defaultOutputModes(List.of("text"))
.skills(List.of(AgentSkill.builder()
.id("stub")
.name("stub")
.description("stub")
.tags(List.of())
.build()))
.supportedInterfaces(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), DEFAULT_SUT_URL),
new AgentInterface(TransportProtocol.GRPC.asString(), DEFAULT_SUT_URL),
new AgentInterface(TransportProtocol.HTTP_JSON.asString(), DEFAULT_SUT_URL)))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {

try {
org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = org.a2aproject.sdk.grpc.AgentCard.newBuilder();
JSONRPCUtils.parseJsonString(body, agentCardBuilder, "");
JSONRPCUtils.parseJsonString(body, agentCardBuilder, "", true);
return ProtoUtils.FromProto.agentCard(agentCardBuilder);
} catch (A2AError | JsonProcessingException e) {
throw new A2AClientJSONError("Could not unmarshal agent card response", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ public interface AgentCardMapper {
@Mapping(target = "provider", source = "provider", conditionExpression = "java(proto.hasProvider())")
@Mapping(target = "documentationUrl", source = "documentationUrl", conditionExpression = "java(!proto.getDocumentationUrl().isEmpty())")
@Mapping(target = "iconUrl", source = "iconUrl", conditionExpression = "java(!proto.getIconUrl().isEmpty())")
@Mapping(target = "url", ignore = true)
@Mapping(target = "preferredTransport", ignore = true)
org.a2aproject.sdk.spec.AgentCard fromProto(org.a2aproject.sdk.grpc.AgentCard proto);
}
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,13 @@ protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.
}

public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id) throws JsonProcessingException {
parseJsonString(body, builder, id, false);
}

public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id, boolean ignoringUnknownFields) throws JsonProcessingException {
try {
JsonFormat.parser().merge(body, builder);
JsonFormat.Parser parser = ignoringUnknownFields ? JsonFormat.parser().ignoringUnknownFields() : JsonFormat.parser();
parser.merge(body, builder);
} catch (InvalidProtocolBufferException e) {
log.log(Level.FINE, "Protocol buffer parsing failed for JSON: {0}", body);
log.log(Level.FINE, "Proto parsing error details", e);
Expand Down
Loading
Loading