From 0003887f3064e2c69f6ca692edba70856178e8e0 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 17:07:00 +0200 Subject: [PATCH 01/15] feat: add Rust-Go interoperability suite Signed-off-by: Luca Muscariello --- integrations/Taskfile.yml | 5 + integrations/agntcy-a2a/.gitignore | 2 + integrations/agntcy-a2a/README.md | 12 + integrations/agntcy-a2a/Taskfile.yml | 22 + .../fixtures/go-jsonrpc-server/main.go | 84 + .../agntcy-a2a/fixtures/rust/Cargo.lock | 2070 +++++++++++++++++ .../agntcy-a2a/fixtures/rust/Cargo.toml | 14 + .../agntcy-a2a/tests/interop_rust_go_test.go | 407 ++++ integrations/agntcy-a2a/tests/suite_test.go | 16 + integrations/go.mod | 23 +- integrations/go.sum | 30 +- 11 files changed, 2663 insertions(+), 22 deletions(-) create mode 100644 integrations/agntcy-a2a/.gitignore create mode 100644 integrations/agntcy-a2a/README.md create mode 100644 integrations/agntcy-a2a/Taskfile.yml create mode 100644 integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go create mode 100644 integrations/agntcy-a2a/fixtures/rust/Cargo.lock create mode 100644 integrations/agntcy-a2a/fixtures/rust/Cargo.toml create mode 100644 integrations/agntcy-a2a/tests/interop_rust_go_test.go create mode 100644 integrations/agntcy-a2a/tests/suite_test.go diff --git a/integrations/Taskfile.yml b/integrations/Taskfile.yml index dbe7d4e8..8900cf11 100644 --- a/integrations/Taskfile.yml +++ b/integrations/Taskfile.yml @@ -7,6 +7,11 @@ version: '3' silent: true includes: + a2a: + taskfile: ./agntcy-a2a/Taskfile.yml + dir: ./agntcy-a2a + excludes: [ default ] + slim: taskfile: ./agntcy-slim/Taskfile.yml dir: ./agntcy-slim diff --git a/integrations/agntcy-a2a/.gitignore b/integrations/agntcy-a2a/.gitignore new file mode 100644 index 00000000..a2c11904 --- /dev/null +++ b/integrations/agntcy-a2a/.gitignore @@ -0,0 +1,2 @@ +fixtures/rust/target/ +reports/ \ No newline at end of file diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md new file mode 100644 index 00000000..c25307b7 --- /dev/null +++ b/integrations/agntcy-a2a/README.md @@ -0,0 +1,12 @@ +# A2A Interoperability CSIT + +This component hosts cross-SDK A2A interoperability checks. + +The initial slice covers a self-contained Rust and Go JSON-RPC smoke matrix: + +- Go client -> Go server +- Go client -> Rust server +- Rust client -> Go server +- Rust client -> Rust server + +The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. \ No newline at end of file diff --git a/integrations/agntcy-a2a/Taskfile.yml b/integrations/agntcy-a2a/Taskfile.yml new file mode 100644 index 00000000..3014f693 --- /dev/null +++ b/integrations/agntcy-a2a/Taskfile.yml @@ -0,0 +1,22 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +--- +version: '3' + +silent: true + +tasks: + test: + desc: All A2A interoperability tests + cmds: + - task: test:rust-go:jsonrpc + + test:rust-go:jsonrpc: + desc: Rust and Go JSON-RPC interoperability smoke test + cmds: + - mkdir -p ./reports + - cd tests && go test . -v -failfast -test.v -test.paniconexit0 -ginkgo.timeout 20m -timeout 20m -ginkgo.v --ginkgo.json-report=../reports/report-agntcy-a2a.json --ginkgo.junit-report=../reports/report-agntcy-a2a.xml + + default: + cmd: task -l \ No newline at end of file diff --git a/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go b/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go new file mode 100644 index 00000000..12aa3666 --- /dev/null +++ b/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go @@ -0,0 +1,84 @@ +// Copyright AGNTCY Contributors (https://github.com/agntcy) +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "flag" + "fmt" + "iter" + "log" + "net" + "net/http" + + "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2asrv" +) + +type interopExecutor struct{} + +func (*interopExecutor) Execute(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { + response := a2a.NewMessage( + a2a.MessageRoleAgent, + a2a.NewTextPart(fmt.Sprintf("go server received: %s", firstText(execCtx.Message))), + ) + + return func(yield func(a2a.Event, error) bool) { + yield(response, nil) + } +} + +func (*interopExecutor) Cancel(_ context.Context, _ *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { + return func(yield func(a2a.Event, error) bool) {} +} + +func firstText(message *a2a.Message) string { + if message == nil { + return "" + } + + for _, part := range message.Parts { + if text := part.Text(); text != "" { + return text + } + } + + return "" +} + +func agentCard(port int) *a2a.AgentCard { + baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) + + return &a2a.AgentCard{ + Name: "CSIT Go JSON-RPC Agent", + Description: "Go interoperability fixture for CSIT", + Version: "1.0.0", + SupportedInterfaces: []*a2a.AgentInterface{ + a2a.NewAgentInterface(baseURL+"/rpc", a2a.TransportProtocolJSONRPC), + }, + Capabilities: a2a.AgentCapabilities{Streaming: true}, + DefaultInputModes: []string{"text/plain"}, + DefaultOutputModes: []string{"text/plain"}, + } +} + +func main() { + port := flag.Int("port", 19091, "port for the JSON-RPC fixture server") + flag.Parse() + + listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port)) + if err != nil { + log.Fatalf("failed to bind listener: %v", err) + } + + handler := a2asrv.NewHandler(&interopExecutor{}) + mux := http.NewServeMux() + mux.Handle("/rpc", a2asrv.NewJSONRPCHandler(handler)) + mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(agentCard(*port))) + + log.Printf("go jsonrpc fixture listening on http://127.0.0.1:%d", *port) + if err := http.Serve(listener, mux); err != nil { + log.Fatalf("server stopped: %v", err) + } +} diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock new file mode 100644 index 00000000..cfec4136 --- /dev/null +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -0,0 +1,2070 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "agntcy-a2a" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f2df4ac201606e523996a81451de09fcd0e64cd99adce17bbc6675df80cd44" +dependencies = [ + "base64", + "chrono", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "agntcy-a2a-csit-fixtures" +version = "0.1.0" +dependencies = [ + "agntcy-a2a", + "agntcy-a2a-server", + "axum", + "futures", + "reqwest", + "serde_json", + "tokio", +] + +[[package]] +name = "agntcy-a2a-server" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4d959fc430280d643ba5b365770d494a8561c160aa90770205d90a487ecb4dc" +dependencies = [ + "agntcy-a2a", + "async-trait", + "axum", + "chrono", + "futures", + "hyper", + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tower-http", + "tracing", + "uuid", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml new file mode 100644 index 00000000..5ee9ddcb --- /dev/null +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "agntcy-a2a-csit-fixtures" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +a2a = { package = "agntcy-a2a", version = "0.2.2" } +a2a-server = { package = "agntcy-a2a-server", version = "0.1.2" } +axum = "0.8" +futures = "0.3" +reqwest = { version = "0.12", features = ["json"] } +serde_json = "1" +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] } \ No newline at end of file diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go new file mode 100644 index 00000000..794bd891 --- /dev/null +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -0,0 +1,407 @@ +// Copyright AGNTCY Contributors (https://github.com/agntcy) +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "bytes" + "context" + "errors" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "time" + + "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + fixtureReadyTimeout = 20 * time.Second + probeTimeout = 2 * time.Minute + buildTimeout = 3 * time.Minute + stopTimeout = 5 * time.Second + requestText = "ping" +) + +type fixtureBinaries struct { + tempDir string + goServer string + rustServer string + rustProbe string +} + +type lockedBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (buffer *lockedBuffer) Write(data []byte) (int, error) { + buffer.mu.Lock() + defer buffer.mu.Unlock() + return buffer.buf.Write(data) +} + +func (buffer *lockedBuffer) String() string { + buffer.mu.Lock() + defer buffer.mu.Unlock() + return buffer.buf.String() +} + +type fixtureProcess struct { + name string + cmd *exec.Cmd + cancel context.CancelFunc + done chan error + logs *lockedBuffer +} + +func (process *fixtureProcess) stop() error { + process.cancel() + + select { + case err := <-process.done: + return normalizeStopError(err) + case <-time.After(stopTimeout): + } + + if process.cmd.Process != nil { + _ = process.cmd.Process.Kill() + } + + select { + case err := <-process.done: + return normalizeStopError(err) + case <-time.After(stopTimeout): + return fmt.Errorf("timed out stopping %s", process.name) + } +} + +func normalizeStopError(err error) error { + if err == nil { + return nil + } + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return nil + } + + return err +} + +func componentRoot() string { + _, currentFile, _, ok := runtime.Caller(0) + if !ok { + panic("failed to determine test file path") + } + + return filepath.Dir(filepath.Dir(currentFile)) +} + +func findFreePort() int { + listener, err := net.Listen("tcp", "127.0.0.1:0") + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + defer listener.Close() + + return listener.Addr().(*net.TCPAddr).Port +} + +func waitForReady(url string, done <-chan error, logs *lockedBuffer) error { + client := &http.Client{Timeout: 500 * time.Millisecond} + deadline := time.Now().Add(fixtureReadyTimeout) + + for time.Now().Before(deadline) { + select { + case err := <-done: + if err == nil { + err = errors.New("process exited before becoming ready") + } + return fmt.Errorf("fixture exited early while waiting for %s: %w\n%s", url, err, logs.String()) + default: + } + + response, err := client.Get(url) + if err == nil { + response.Body.Close() + if response.StatusCode == http.StatusOK { + return nil + } + } + + time.Sleep(200 * time.Millisecond) + } + + return fmt.Errorf("timed out waiting for fixture readiness at %s\n%s", url, logs.String()) +} + +func executableName(name string) string { + if runtime.GOOS == "windows" { + return name + ".exe" + } + + return name +} + +func buildFixtureBinaries() (fixtureBinaries, error) { + root := componentRoot() + tempDir, err := os.MkdirTemp("", "agntcy-a2a-binaries-") + if err != nil { + return fixtureBinaries{}, fmt.Errorf("create temp dir: %w", err) + } + + binaries := fixtureBinaries{ + tempDir: tempDir, + goServer: filepath.Join(tempDir, executableName("go-jsonrpc-server")), + rustServer: filepath.Join(tempDir, "cargo-target", "debug", executableName("interop-rust-server")), + rustProbe: filepath.Join(tempDir, "cargo-target", "debug", executableName("interop-rust-probe")), + } + + buildCtx, cancel := context.WithTimeout(context.Background(), buildTimeout) + defer cancel() + + goBuild := exec.CommandContext(buildCtx, "go", "build", "-o", binaries.goServer, "./fixtures/go-jsonrpc-server") + goBuild.Dir = root + if output, err := goBuild.CombinedOutput(); err != nil { + _ = os.RemoveAll(tempDir) + return fixtureBinaries{}, fmt.Errorf("build go fixture: %w\n%s", err, string(output)) + } + + rustBuild := exec.CommandContext( + buildCtx, + "cargo", + "build", + "--manifest-path", + filepath.Join(root, "fixtures", "rust", "Cargo.toml"), + "--bins", + "--target-dir", + filepath.Join(tempDir, "cargo-target"), + ) + rustBuild.Dir = root + if output, err := rustBuild.CombinedOutput(); err != nil { + _ = os.RemoveAll(tempDir) + return fixtureBinaries{}, fmt.Errorf("build rust fixtures: %w\n%s", err, string(output)) + } + + return binaries, nil +} + +func startFixtureProcess(name string, dir string, readyURL string, command string, args ...string) (*fixtureProcess, error) { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, command, args...) + cmd.Dir = dir + + logs := &lockedBuffer{} + cmd.Stdout = logs + cmd.Stderr = logs + + if err := cmd.Start(); err != nil { + cancel() + return nil, fmt.Errorf("start %s: %w", name, err) + } + + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + if err := waitForReady(readyURL, done, logs); err != nil { + cancel() + <-done + return nil, fmt.Errorf("wait for %s readiness: %w", name, err) + } + + return &fixtureProcess{name: name, cmd: cmd, cancel: cancel, done: done, logs: logs}, nil +} + +func startGoFixture(binaries fixtureBinaries, port int) (*fixtureProcess, string, error) { + baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) + process, err := startFixtureProcess( + "go-jsonrpc-server", + componentRoot(), + baseURL+"/.well-known/agent-card.json", + binaries.goServer, + "--port", + fmt.Sprintf("%d", port), + ) + return process, baseURL, err +} + +func startRustFixture(binaries fixtureBinaries, port int) (*fixtureProcess, string, error) { + baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) + process, err := startFixtureProcess( + "interop-rust-server", + componentRoot(), + baseURL+"/.well-known/agent-card.json", + binaries.rustServer, + "--port", + fmt.Sprintf("%d", port), + ) + return process, baseURL, err +} + +func newGoClient(ctx context.Context, baseURL string) (*a2aclient.Client, error) { + card, err := agentcard.DefaultResolver.Resolve(ctx, baseURL) + if err != nil { + return nil, err + } + + return a2aclient.NewFromCard(ctx, card) +} + +func firstMessageText(message *a2a.Message) (string, error) { + for _, part := range message.Parts { + if text := part.Text(); text != "" { + return text, nil + } + } + + return "", errors.New("message did not include a text part") +} + +func goClientUnaryText(ctx context.Context, client *a2aclient.Client) (string, error) { + result, err := client.SendMessage(ctx, &a2a.SendMessageRequest{ + Message: a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart(requestText)), + }) + if err != nil { + return "", err + } + + message, ok := result.(*a2a.Message) + if !ok { + return "", fmt.Errorf("unexpected unary response type %T", result) + } + + return firstMessageText(message) +} + +func goClientStreamingText(ctx context.Context, client *a2aclient.Client) (string, error) { + request := &a2a.SendMessageRequest{ + Message: a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart(requestText)), + } + + for event, err := range client.SendStreamingMessage(ctx, request) { + if err != nil { + return "", err + } + + message, ok := event.(*a2a.Message) + if !ok { + continue + } + + return firstMessageText(message) + } + + return "", errors.New("stream completed without a message event") +} + +func runRustProbe(ctx context.Context, binaries fixtureBinaries, baseURL string, expectedText string) (string, error) { + cmd := exec.CommandContext( + ctx, + binaries.rustProbe, + "--card-url", + baseURL, + "--expect-text", + expectedText, + ) + cmd.Dir = componentRoot() + + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), fmt.Errorf("rust probe failed: %w\n%s", err, string(output)) + } + + return string(output), nil +} + +var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func() { + var ( + binaries fixtureBinaries + goFixture *fixtureProcess + rustFixture *fixtureProcess + goFixtureURL string + rustFixtureURL string + ) + + ginkgo.BeforeAll(func() { + var err error + + binaries, err = buildFixtureBinaries() + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + goFixture, goFixtureURL, err = startGoFixture(binaries, findFreePort()) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + rustFixture, rustFixtureURL, err = startRustFixture(binaries, findFreePort()) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + }) + + ginkgo.AfterAll(func() { + if rustFixture != nil { + gomega.Expect(rustFixture.stop()).To(gomega.Succeed(), rustFixture.logs.String()) + } + if goFixture != nil { + gomega.Expect(goFixture.stop()).To(gomega.Succeed(), goFixture.logs.String()) + } + if binaries.tempDir != "" { + gomega.Expect(os.RemoveAll(binaries.tempDir)).To(gomega.Succeed()) + } + }) + + ginkgo.It("lets the Go client call the Go fixture", func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + client, err := newGoClient(requestCtx, goFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal("go server received: ping")) + + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal("go server received: ping")) + }) + + ginkgo.It("lets the Go client call the Rust fixture", func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + client, err := newGoClient(requestCtx, rustFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal("rust server received: ping")) + + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal("rust server received: ping")) + }) + + ginkgo.It("lets the Rust client call the Go fixture", func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, goFixtureURL, "go server received: ping") + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) + + ginkgo.It("lets the Rust client call the Rust fixture", func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, rustFixtureURL, "rust server received: ping") + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) +}) diff --git a/integrations/agntcy-a2a/tests/suite_test.go b/integrations/agntcy-a2a/tests/suite_test.go new file mode 100644 index 00000000..88160aba --- /dev/null +++ b/integrations/agntcy-a2a/tests/suite_test.go @@ -0,0 +1,16 @@ +// Copyright AGNTCY Contributors (https://github.com/agntcy) +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "testing" + + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +func TestTests(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "A2A Interop Suite") +} diff --git a/integrations/go.mod b/integrations/go.mod index 499adba5..897e4f84 100644 --- a/integrations/go.mod +++ b/integrations/go.mod @@ -1,10 +1,9 @@ module github.com/agntcy/csit/integrations -go 1.24.0 - -toolchain go1.24.1 +go 1.24.4 require ( + github.com/a2aproject/a2a-go/v2 v2.1.0 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.0 github.com/stretchr/testify v1.10.0 @@ -13,7 +12,11 @@ require ( k8s.io/client-go v0.33.0 ) -require github.com/google/go-cmp v0.7.0 // indirect +require ( + github.com/google/go-cmp v0.7.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sync v0.19.0 // indirect +) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -38,14 +41,14 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.26.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 diff --git a/integrations/go.sum b/integrations/go.sum index 27adae7f..7e3d6b43 100644 --- a/integrations/go.sum +++ b/integrations/go.sum @@ -1,3 +1,5 @@ +github.com/a2aproject/a2a-go/v2 v2.1.0 h1:mtn3UR+B8RnIRYTVNUHmip8FMCK5Pe3Id2aNATOWEPw= +github.com/a2aproject/a2a-go/v2 v2.1.0/go.mod h1:nm/NLcGWEQsVf7rgcLy74DyswS5BanPS6ubQhGhiQaA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -86,42 +88,46 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 56e85da4990ee41b894447b83d9c943e99755ef3 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 17:07:48 +0200 Subject: [PATCH 02/15] fix: include Rust interop fixture sources Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/.gitignore | 4 +- .../rust/src/bin/interop-rust-probe.rs | 238 ++++++++++++++++++ .../rust/src/bin/interop-rust-server.rs | 163 ++++++++++++ 3 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs create mode 100644 integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs diff --git a/integrations/agntcy-a2a/.gitignore b/integrations/agntcy-a2a/.gitignore index a2c11904..54ab67f1 100644 --- a/integrations/agntcy-a2a/.gitignore +++ b/integrations/agntcy-a2a/.gitignore @@ -1,2 +1,4 @@ fixtures/rust/target/ -reports/ \ No newline at end of file +reports/ +!fixtures/rust/src/bin/ +!fixtures/rust/src/bin/*.rs \ No newline at end of file diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs new file mode 100644 index 00000000..e5602e37 --- /dev/null +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs @@ -0,0 +1,238 @@ +// Copyright AGNTCY Contributors (https://github.com/agntcy) +// SPDX-License-Identifier: Apache-2.0 + +use std::env; +use std::process; + +use a2a::*; +use reqwest::header::ACCEPT; +use serde_json::Value; + +const METHOD_SEND_MESSAGE: &str = "SendMessage"; +const METHOD_SEND_STREAMING_MESSAGE: &str = "SendStreamingMessage"; + +struct Args { + card_url: String, + expect_text: String, +} + +fn parse_args() -> Result { + let mut args = env::args().skip(1); + let mut card_url = None; + let mut expect_text = None; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--card-url" => { + card_url = Some( + args.next() + .ok_or_else(|| "--card-url requires a value".to_string())?, + ); + } + "--expect-text" => { + expect_text = Some( + args.next() + .ok_or_else(|| "--expect-text requires a value".to_string())?, + ); + } + other => { + return Err(format!("unknown argument: {other}")); + } + } + } + + Ok(Args { + card_url: card_url.ok_or_else(|| "missing --card-url".to_string())?, + expect_text: expect_text.ok_or_else(|| "missing --expect-text".to_string())?, + }) +} + +fn first_text(message: &Message) -> Result { + message + .parts + .iter() + .find_map(Part::as_text) + .map(ToString::to_string) + .ok_or_else(|| "message contained no text parts".to_string()) +} + +fn assert_event_text(response: StreamResponse, expected: &str) -> Result<(), String> { + match response { + StreamResponse::Message(message) => { + let actual = first_text(&message)?; + if actual == expected { + Ok(()) + } else { + Err(format!( + "unexpected unary response text: got {actual:?}, want {expected:?}" + )) + } + } + other => Err(format!("unexpected response event: {other:?}")), + } +} + +fn extract_jsonrpc_result(response: JsonRpcResponse) -> Result { + if let Some(error) = response.error { + return Err(format!("jsonrpc error {}: {}", error.code, error.message)); + } + + response + .result + .ok_or_else(|| "jsonrpc response missing result".to_string()) +} + +fn resolve_jsonrpc_url(card: &Value) -> Result { + let interfaces = card + .get("supportedInterfaces") + .and_then(Value::as_array) + .ok_or_else(|| "agent card has no supportedInterfaces array".to_string())?; + + interfaces + .iter() + .find(|interface| { + interface.get("protocolBinding").and_then(Value::as_str) + == Some(TRANSPORT_PROTOCOL_JSONRPC) + }) + .and_then(|interface| interface.get("url")) + .and_then(Value::as_str) + .map(ToString::to_string) + .ok_or_else(|| "agent card has no JSON-RPC interface".to_string()) +} + +async fn resolve_agent_card(client: &reqwest::Client, card_url: &str) -> Result { + let url = format!( + "{}/.well-known/agent-card.json", + card_url.trim_end_matches('/') + ); + let response = client + .get(url) + .send() + .await + .map_err(|error| format!("agent card fetch failed: {error}"))?; + + if !response.status().is_success() { + return Err(format!( + "agent card fetch returned HTTP {}", + response.status() + )); + } + + response + .json::() + .await + .map_err(|error| format!("agent card parse failed: {error}")) +} + +async fn send_unary( + client: &reqwest::Client, + endpoint: &str, + request: &SendMessageRequest, +) -> Result { + let rpc = JsonRpcRequest::new( + JsonRpcId::String("interop-unary".to_string()), + METHOD_SEND_MESSAGE, + Some(serde_json::to_value(request).map_err(|error| error.to_string())?), + ); + let response = client + .post(endpoint) + .json(&rpc) + .send() + .await + .map_err(|error| format!("unary request failed: {error}"))?; + + if !response.status().is_success() { + return Err(format!("unary request returned HTTP {}", response.status())); + } + + let rpc_response = response + .json::() + .await + .map_err(|error| format!("failed to parse unary response: {error}"))?; + serde_json::from_value(extract_jsonrpc_result(rpc_response)?) + .map_err(|error| format!("failed to decode unary result: {error}")) +} + +async fn send_streaming( + client: &reqwest::Client, + endpoint: &str, + request: &SendMessageRequest, +) -> Result { + let rpc = JsonRpcRequest::new( + JsonRpcId::String("interop-stream".to_string()), + METHOD_SEND_STREAMING_MESSAGE, + Some(serde_json::to_value(request).map_err(|error| error.to_string())?), + ); + let response = client + .post(endpoint) + .header(ACCEPT, "text/event-stream") + .json(&rpc) + .send() + .await + .map_err(|error| format!("streaming request failed: {error}"))?; + + if !response.status().is_success() { + return Err(format!( + "streaming request returned HTTP {}", + response.status() + )); + } + + let body = response + .text() + .await + .map_err(|error| format!("failed to read streaming response: {error}"))?; + + for line in body.lines() { + let Some(payload) = line.strip_prefix("data:") else { + continue; + }; + + let rpc_response = serde_json::from_str::(payload.trim()) + .map_err(|error| format!("failed to parse SSE payload: {error}"))?; + return serde_json::from_value(extract_jsonrpc_result(rpc_response)?) + .map_err(|error| format!("failed to decode streaming result: {error}")); + } + + Err("stream completed without a data event".to_string()) +} + +#[tokio::main] +async fn main() { + let args = match parse_args() { + Ok(args) => args, + Err(error) => { + eprintln!("{error}"); + process::exit(2); + } + }; + + if let Err(error) = run(args).await { + eprintln!("{error}"); + process::exit(1); + } +} + +async fn run(args: Args) -> Result<(), String> { + let client = reqwest::Client::new(); + let card = resolve_agent_card(&client, &args.card_url) + .await + .map_err(|error| format!("agent card resolution failed: {error}"))?; + let endpoint = resolve_jsonrpc_url(&card)?; + + let request = SendMessageRequest { + message: Message::new(Role::User, vec![Part::text("ping")]), + configuration: None, + metadata: None, + tenant: None, + }; + + let response = send_unary(&client, &endpoint, &request).await?; + assert_event_text(response, &args.expect_text)?; + + let stream_event = send_streaming(&client, &endpoint, &request).await?; + assert_event_text(stream_event, &args.expect_text)?; + + println!("validated {0} against {1}", args.expect_text, args.card_url); + Ok(()) +} diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs new file mode 100644 index 00000000..295de35c --- /dev/null +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs @@ -0,0 +1,163 @@ +// Copyright AGNTCY Contributors (https://github.com/agntcy) +// SPDX-License-Identifier: Apache-2.0 + +use std::env; + +use a2a::*; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use futures::stream::{self, BoxStream}; +use serde_json::Value; +use tokio::net::TcpListener; + +const METHOD_SEND_MESSAGE: &str = "SendMessage"; +const METHOD_SEND_STREAMING_MESSAGE: &str = "SendStreamingMessage"; +const LEGACY_METHOD_SEND_MESSAGE: &str = "message.send"; +const LEGACY_METHOD_SEND_STREAMING_MESSAGE: &str = "message.stream"; + +#[derive(Clone)] +struct AppState { + card: AgentCard, +} + +fn first_text(message: Option<&Message>) -> String { + message + .and_then(|message| message.parts.iter().find_map(Part::as_text)) + .unwrap_or_default() + .to_string() +} + +fn build_agent_card(port: u16) -> AgentCard { + let base_url = format!("http://127.0.0.1:{port}"); + + AgentCard { + name: "CSIT Rust JSON-RPC Agent".to_string(), + description: "Rust interoperability fixture for CSIT".to_string(), + version: VERSION.to_string(), + supported_interfaces: vec![AgentInterface::new( + format!("{base_url}/rpc"), + TRANSPORT_PROTOCOL_JSONRPC, + )], + capabilities: AgentCapabilities { + streaming: Some(true), + push_notifications: Some(false), + extensions: None, + extended_agent_card: None, + }, + default_input_modes: vec!["text/plain".to_string()], + default_output_modes: vec!["text/plain".to_string()], + skills: vec![], + provider: None, + documentation_url: None, + icon_url: None, + security_schemes: None, + security_requirements: None, + signatures: None, + } +} + +fn build_response_message(request: &SendMessageRequest) -> Message { + Message::new( + Role::Agent, + vec![Part::text(format!( + "rust server received: {}", + first_text(Some(&request.message)) + ))], + ) +} + +fn error_response(id: JsonRpcId, error: A2AError) -> Json { + Json(JsonRpcResponse::error(id, error.to_jsonrpc_error())) +} + +async fn handle_agent_card(State(state): State) -> Json { + Json(state.card) +} + +async fn handle_jsonrpc( + State(_state): State, + Json(request): Json, +) -> impl IntoResponse { + let id = request.id.clone(); + let raw_params = request.params.unwrap_or(Value::Null); + + if request.jsonrpc != "2.0" { + return error_response(id, A2AError::invalid_request("invalid jsonrpc version")) + .into_response(); + } + + match request.method.as_str() { + METHOD_SEND_MESSAGE | LEGACY_METHOD_SEND_MESSAGE => { + match serde_json::from_value::(raw_params) { + Ok(request) => { + let response = StreamResponse::Message(build_response_message(&request)); + let value = serde_json::to_value(response) + .map_err(|error| A2AError::internal(error.to_string())); + + match value { + Ok(value) => Json(JsonRpcResponse::success(id, value)).into_response(), + Err(error) => error_response(id, error).into_response(), + } + } + Err(error) => error_response( + id, + A2AError::invalid_request(format!("invalid params: {error}")), + ) + .into_response(), + } + } + METHOD_SEND_STREAMING_MESSAGE | LEGACY_METHOD_SEND_STREAMING_MESSAGE => { + match serde_json::from_value::(raw_params) { + Ok(request) => { + let response = StreamResponse::Message(build_response_message(&request)); + let stream: BoxStream<'static, Result> = + Box::pin(stream::once(async move { Ok(response) })); + a2a_server::sse::sse_jsonrpc_stream(id, stream).into_response() + } + Err(error) => error_response( + id, + A2AError::invalid_request(format!("invalid params: {error}")), + ) + .into_response(), + } + } + "" => error_response(id, A2AError::invalid_request("method is required")).into_response(), + _ => error_response(id, A2AError::method_not_found(&request.method)).into_response(), + } +} + +fn parse_port() -> u16 { + let mut args = env::args().skip(1); + + while let Some(arg) = args.next() { + if arg == "--port" { + let value = args.next().expect("--port requires a numeric argument"); + return value.parse::().expect("--port value must fit in u16"); + } + } + + 19092 +} + +#[tokio::main] +async fn main() { + let port = parse_port(); + let state = AppState { + card: build_agent_card(port), + }; + + let app = Router::new() + .route("/rpc", post(handle_jsonrpc)) + .route("/jsonrpc", post(handle_jsonrpc)) + .route("/.well-known/agent-card.json", get(handle_agent_card)) + .with_state(state); + + let listener = TcpListener::bind(("127.0.0.1", port)) + .await + .expect("listener should bind"); + + println!("rust jsonrpc fixture listening on http://127.0.0.1:{port}"); + axum::serve(listener, app).await.expect("server should run"); +} From eb73ac93d9aa3aaf56e531ea2381e4381355aae8 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 17:10:43 +0200 Subject: [PATCH 03/15] ci: add Rust-Go interop integration job Signed-off-by: Luca Muscariello --- .github/workflows/test-integrations.yaml | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/test-integrations.yaml b/.github/workflows/test-integrations.yaml index 64b3a285..45cbaf54 100644 --- a/.github/workflows/test-integrations.yaml +++ b/.github/workflows/test-integrations.yaml @@ -44,6 +44,11 @@ on: required: false default: true type: boolean + skip_a2a_test: + description: "Skip A2A Rust-Go interoperability tests" + required: false + default: false + type: boolean run_agentic_apps_test: description: "Run agentic apps tests" required: false @@ -177,6 +182,38 @@ jobs: with: artifact-name: "directory-test-result" + run-tests-a2a: + if: ${{ github.event_name != 'workflow_dispatch' || !inputs.skip_a2a_test }} + runs-on: ubuntu-latest + + permissions: + contents: "read" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Environment + uses: ./.github/actions/setup-env + with: + go: true + go-version: "1.24.4" + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Run A2A interoperability tests + run: task integrations:a2a:test:rust-go:jsonrpc + shell: bash + + - name: Create artifact from integrations test reports + uses: ./.github/actions/create-artifact + with: + artifact-name: "a2a-rust-go-test-result" + path-to-archive: "./integrations/agntcy-a2a/reports/*" + run-agentic-apps: if: ${{ inputs.run_agentic_apps_test }} runs-on: ubuntu-latest @@ -208,6 +245,7 @@ jobs: [ run-tests-slim-topology, run-tests-directory, + run-tests-a2a, ] if: ${{ always() }} runs-on: ubuntu-latest From f452052f05d94a438aeda0f41a557aab86c93093 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 18:28:54 +0200 Subject: [PATCH 04/15] test: use released rust sdk interop paths Signed-off-by: Luca Muscariello --- .../agntcy-a2a/fixtures/rust/Cargo.lock | 32 ++- .../agntcy-a2a/fixtures/rust/Cargo.toml | 7 +- .../rust/src/bin/interop-rust-probe.rs | 209 ++++++------------ .../rust/src/bin/interop-rust-server.rs | 103 +++------ 4 files changed, 119 insertions(+), 232 deletions(-) diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index cfec4136..be74c793 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "agntcy-a2a" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f2df4ac201606e523996a81451de09fcd0e64cd99adce17bbc6675df80cd44" +checksum = "042c4a30365eb47860f3202259b174f1ddefe5206331f364d6072daf106d9d38" dependencies = [ "base64", "chrono", @@ -16,24 +16,44 @@ dependencies = [ "uuid", ] +[[package]] +name = "agntcy-a2a-client" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea81781417ec53efef6249b8bd76fb88d4c5f3078247cf28d7af95365bc41cf" +dependencies = [ + "agntcy-a2a", + "async-trait", + "bytes", + "futures", + "pin-project-lite", + "reqwest", + "semver", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + [[package]] name = "agntcy-a2a-csit-fixtures" version = "0.1.0" dependencies = [ "agntcy-a2a", + "agntcy-a2a-client", "agntcy-a2a-server", "axum", "futures", - "reqwest", - "serde_json", "tokio", ] [[package]] name = "agntcy-a2a-server" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4d959fc430280d643ba5b365770d494a8561c160aa90770205d90a487ecb4dc" +checksum = "20b154eaf616b336d9dcf5a7c7c8ff42a7da1c3f0ebee72e19216ed06245d54f" dependencies = [ "agntcy-a2a", "async-trait", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index 5ee9ddcb..dd053347 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -5,10 +5,9 @@ edition = "2024" publish = false [dependencies] -a2a = { package = "agntcy-a2a", version = "0.2.2" } -a2a-server = { package = "agntcy-a2a-server", version = "0.1.2" } +a2a = { package = "agntcy-a2a", version = "0.2.3" } +a2a-client = { package = "agntcy-a2a-client", version = "0.1.6" } +a2a-server = { package = "agntcy-a2a-server", version = "0.1.3" } axum = "0.8" futures = "0.3" -reqwest = { version = "0.12", features = ["json"] } -serde_json = "1" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] } \ No newline at end of file diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs index e5602e37..06d2ec4c 100644 --- a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs @@ -5,11 +5,9 @@ use std::env; use std::process; use a2a::*; -use reqwest::header::ACCEPT; -use serde_json::Value; - -const METHOD_SEND_MESSAGE: &str = "SendMessage"; -const METHOD_SEND_STREAMING_MESSAGE: &str = "SendStreamingMessage"; +use a2a_client::A2AClientFactory; +use a2a_client::agent_card::AgentCardResolver; +use futures::StreamExt; struct Args { card_url: String, @@ -56,145 +54,39 @@ fn first_text(message: &Message) -> Result { .ok_or_else(|| "message contained no text parts".to_string()) } -fn assert_event_text(response: StreamResponse, expected: &str) -> Result<(), String> { - match response { - StreamResponse::Message(message) => { - let actual = first_text(&message)?; - if actual == expected { - Ok(()) - } else { - Err(format!( - "unexpected unary response text: got {actual:?}, want {expected:?}" - )) - } - } - other => Err(format!("unexpected response event: {other:?}")), - } -} - -fn extract_jsonrpc_result(response: JsonRpcResponse) -> Result { - if let Some(error) = response.error { - return Err(format!("jsonrpc error {}: {}", error.code, error.message)); +fn assert_text(actual: String, expected: &str, kind: &str) -> Result<(), String> { + if actual == expected { + Ok(()) + } else { + Err(format!( + "unexpected {kind} response text: got {actual:?}, want {expected:?}" + )) } - - response - .result - .ok_or_else(|| "jsonrpc response missing result".to_string()) -} - -fn resolve_jsonrpc_url(card: &Value) -> Result { - let interfaces = card - .get("supportedInterfaces") - .and_then(Value::as_array) - .ok_or_else(|| "agent card has no supportedInterfaces array".to_string())?; - - interfaces - .iter() - .find(|interface| { - interface.get("protocolBinding").and_then(Value::as_str) - == Some(TRANSPORT_PROTOCOL_JSONRPC) - }) - .and_then(|interface| interface.get("url")) - .and_then(Value::as_str) - .map(ToString::to_string) - .ok_or_else(|| "agent card has no JSON-RPC interface".to_string()) } -async fn resolve_agent_card(client: &reqwest::Client, card_url: &str) -> Result { - let url = format!( - "{}/.well-known/agent-card.json", - card_url.trim_end_matches('/') - ); - let response = client - .get(url) - .send() - .await - .map_err(|error| format!("agent card fetch failed: {error}"))?; - - if !response.status().is_success() { - return Err(format!( - "agent card fetch returned HTTP {}", - response.status() - )); - } - - response - .json::() - .await - .map_err(|error| format!("agent card parse failed: {error}")) -} - -async fn send_unary( - client: &reqwest::Client, - endpoint: &str, - request: &SendMessageRequest, -) -> Result { - let rpc = JsonRpcRequest::new( - JsonRpcId::String("interop-unary".to_string()), - METHOD_SEND_MESSAGE, - Some(serde_json::to_value(request).map_err(|error| error.to_string())?), - ); - let response = client - .post(endpoint) - .json(&rpc) - .send() - .await - .map_err(|error| format!("unary request failed: {error}"))?; - - if !response.status().is_success() { - return Err(format!("unary request returned HTTP {}", response.status())); +fn unary_text(response: SendMessageResponse) -> Result { + match response { + SendMessageResponse::Message(message) => first_text(&message), + SendMessageResponse::Task(task) => task + .status + .message + .as_ref() + .ok_or_else(|| "task response contained no message".to_string()) + .and_then(first_text), } - - let rpc_response = response - .json::() - .await - .map_err(|error| format!("failed to parse unary response: {error}"))?; - serde_json::from_value(extract_jsonrpc_result(rpc_response)?) - .map_err(|error| format!("failed to decode unary result: {error}")) } -async fn send_streaming( - client: &reqwest::Client, - endpoint: &str, - request: &SendMessageRequest, -) -> Result { - let rpc = JsonRpcRequest::new( - JsonRpcId::String("interop-stream".to_string()), - METHOD_SEND_STREAMING_MESSAGE, - Some(serde_json::to_value(request).map_err(|error| error.to_string())?), - ); - let response = client - .post(endpoint) - .header(ACCEPT, "text/event-stream") - .json(&rpc) - .send() - .await - .map_err(|error| format!("streaming request failed: {error}"))?; - - if !response.status().is_success() { - return Err(format!( - "streaming request returned HTTP {}", - response.status() - )); - } - - let body = response - .text() - .await - .map_err(|error| format!("failed to read streaming response: {error}"))?; - - for line in body.lines() { - let Some(payload) = line.strip_prefix("data:") else { - continue; - }; - - let rpc_response = serde_json::from_str::(payload.trim()) - .map_err(|error| format!("failed to parse SSE payload: {error}"))?; - return serde_json::from_value(extract_jsonrpc_result(rpc_response)?) - .map_err(|error| format!("failed to decode streaming result: {error}")); +fn streaming_text(response: StreamResponse) -> Result { + match response { + StreamResponse::Message(message) => first_text(&message), + StreamResponse::Task(task) => task + .status + .message + .as_ref() + .ok_or_else(|| "stream task event contained no message".to_string()) + .and_then(first_text), + other => Err(format!("unexpected streaming event: {other:?}")), } - - Err("stream completed without a data event".to_string()) } #[tokio::main] @@ -214,11 +106,16 @@ async fn main() { } async fn run(args: Args) -> Result<(), String> { - let client = reqwest::Client::new(); - let card = resolve_agent_card(&client, &args.card_url) + let resolver = AgentCardResolver::new(None); + let card = resolver + .resolve(&args.card_url) .await .map_err(|error| format!("agent card resolution failed: {error}"))?; - let endpoint = resolve_jsonrpc_url(&card)?; + let client = A2AClientFactory::builder() + .build() + .create_from_card(&card) + .await + .map_err(|error| format!("client creation failed: {error}"))?; let request = SendMessageRequest { message: Message::new(Role::User, vec![Part::text("ping")]), @@ -227,11 +124,35 @@ async fn run(args: Args) -> Result<(), String> { tenant: None, }; - let response = send_unary(&client, &endpoint, &request).await?; - assert_event_text(response, &args.expect_text)?; + let response = client + .send_message(&request) + .await + .map_err(|error| format!("unary request failed: {error}"))?; + assert_text(unary_text(response)?, &args.expect_text, "unary")?; + + let mut stream = client + .send_streaming_message(&request) + .await + .map_err(|error| format!("streaming request failed: {error}"))?; - let stream_event = send_streaming(&client, &endpoint, &request).await?; - assert_event_text(stream_event, &args.expect_text)?; + let stream_event = loop { + match stream.next().await { + Some(Ok(StreamResponse::StatusUpdate(_))) => continue, + Some(Ok(StreamResponse::ArtifactUpdate(_))) => continue, + Some(Ok(event)) => break event, + Some(Err(error)) => { + return Err(format!("streaming event failed: {error}")); + } + None => { + return Err("stream completed without a terminal response event".to_string()); + } + } + }; + assert_text( + streaming_text(stream_event)?, + &args.expect_text, + "streaming", + )?; println!("validated {0} against {1}", args.expect_text, args.card_url); Ok(()) diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs index 295de35c..a43da507 100644 --- a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs @@ -2,25 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use std::env; +use std::sync::Arc; use a2a::*; -use axum::extract::State; -use axum::response::IntoResponse; -use axum::routing::{get, post}; -use axum::{Json, Router}; +use a2a_server::{DefaultRequestHandler, InMemoryTaskStore, StaticAgentCard}; +use axum::Router; use futures::stream::{self, BoxStream}; -use serde_json::Value; use tokio::net::TcpListener; -const METHOD_SEND_MESSAGE: &str = "SendMessage"; -const METHOD_SEND_STREAMING_MESSAGE: &str = "SendStreamingMessage"; -const LEGACY_METHOD_SEND_MESSAGE: &str = "message.send"; -const LEGACY_METHOD_SEND_STREAMING_MESSAGE: &str = "message.stream"; - -#[derive(Clone)] -struct AppState { - card: AgentCard, -} +struct InteropExecutor; fn first_text(message: Option<&Message>) -> String { message @@ -58,73 +48,30 @@ fn build_agent_card(port: u16) -> AgentCard { } } -fn build_response_message(request: &SendMessageRequest) -> Message { +fn build_response_message(request: Option<&Message>) -> Message { Message::new( Role::Agent, vec![Part::text(format!( "rust server received: {}", - first_text(Some(&request.message)) + first_text(request) ))], ) } -fn error_response(id: JsonRpcId, error: A2AError) -> Json { - Json(JsonRpcResponse::error(id, error.to_jsonrpc_error())) -} - -async fn handle_agent_card(State(state): State) -> Json { - Json(state.card) -} - -async fn handle_jsonrpc( - State(_state): State, - Json(request): Json, -) -> impl IntoResponse { - let id = request.id.clone(); - let raw_params = request.params.unwrap_or(Value::Null); - - if request.jsonrpc != "2.0" { - return error_response(id, A2AError::invalid_request("invalid jsonrpc version")) - .into_response(); +impl a2a_server::AgentExecutor for InteropExecutor { + fn execute( + &self, + ctx: a2a_server::ExecutorContext, + ) -> BoxStream<'static, Result> { + let response = StreamResponse::Message(build_response_message(ctx.message.as_ref())); + Box::pin(stream::once(async move { Ok(response) })) } - match request.method.as_str() { - METHOD_SEND_MESSAGE | LEGACY_METHOD_SEND_MESSAGE => { - match serde_json::from_value::(raw_params) { - Ok(request) => { - let response = StreamResponse::Message(build_response_message(&request)); - let value = serde_json::to_value(response) - .map_err(|error| A2AError::internal(error.to_string())); - - match value { - Ok(value) => Json(JsonRpcResponse::success(id, value)).into_response(), - Err(error) => error_response(id, error).into_response(), - } - } - Err(error) => error_response( - id, - A2AError::invalid_request(format!("invalid params: {error}")), - ) - .into_response(), - } - } - METHOD_SEND_STREAMING_MESSAGE | LEGACY_METHOD_SEND_STREAMING_MESSAGE => { - match serde_json::from_value::(raw_params) { - Ok(request) => { - let response = StreamResponse::Message(build_response_message(&request)); - let stream: BoxStream<'static, Result> = - Box::pin(stream::once(async move { Ok(response) })); - a2a_server::sse::sse_jsonrpc_stream(id, stream).into_response() - } - Err(error) => error_response( - id, - A2AError::invalid_request(format!("invalid params: {error}")), - ) - .into_response(), - } - } - "" => error_response(id, A2AError::invalid_request("method is required")).into_response(), - _ => error_response(id, A2AError::method_not_found(&request.method)).into_response(), + fn cancel( + &self, + _ctx: a2a_server::ExecutorContext, + ) -> BoxStream<'static, Result> { + Box::pin(stream::empty()) } } @@ -144,15 +91,15 @@ fn parse_port() -> u16 { #[tokio::main] async fn main() { let port = parse_port(); - let state = AppState { - card: build_agent_card(port), - }; + let handler = Arc::new(DefaultRequestHandler::new( + InteropExecutor, + InMemoryTaskStore::new(), + )); + let card_producer = Arc::new(StaticAgentCard::new(build_agent_card(port))); let app = Router::new() - .route("/rpc", post(handle_jsonrpc)) - .route("/jsonrpc", post(handle_jsonrpc)) - .route("/.well-known/agent-card.json", get(handle_agent_card)) - .with_state(state); + .nest("/rpc", a2a_server::jsonrpc::jsonrpc_router(handler)) + .merge(a2a_server::agent_card::agent_card_router(card_producer)); let listener = TcpListener::bind(("127.0.0.1", port)) .await From 2400403b175211b53a3e19318a6dbec93909d599 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 18:50:59 +0200 Subject: [PATCH 05/15] test: add granular a2a interop tasks Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/README.md | 10 +++- integrations/agntcy-a2a/Taskfile.yml | 47 ++++++++++++++++++- .../agntcy-a2a/tests/interop_rust_go_test.go | 8 ++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index c25307b7..a3ffd052 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -9,4 +9,12 @@ The initial slice covers a self-contained Rust and Go JSON-RPC smoke matrix: - Rust client -> Go server - Rust client -> Rust server -The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. \ No newline at end of file +The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. + +Available task targets: + +- `task test` or `task test:rust-go:jsonrpc` runs the full matrix. +- `task test:rust-go:jsonrpc:go-go` runs the Go client -> Go server case. +- `task test:rust-go:jsonrpc:go-rust` runs the Go client -> Rust server case. +- `task test:rust-go:jsonrpc:rust-go` runs the Rust client -> Go server case. +- `task test:rust-go:jsonrpc:rust-rust` runs the Rust client -> Rust server case. \ No newline at end of file diff --git a/integrations/agntcy-a2a/Taskfile.yml b/integrations/agntcy-a2a/Taskfile.yml index 3014f693..b0410688 100644 --- a/integrations/agntcy-a2a/Taskfile.yml +++ b/integrations/agntcy-a2a/Taskfile.yml @@ -14,9 +14,54 @@ tasks: test:rust-go:jsonrpc: desc: Rust and Go JSON-RPC interoperability smoke test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + REPORT_NAME: report-agntcy-a2a + + test:rust-go:jsonrpc:go-go: + desc: Go client to Go server JSON-RPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: go-go + REPORT_NAME: report-agntcy-a2a-go-go + + test:rust-go:jsonrpc:go-rust: + desc: Go client to Rust server JSON-RPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: go-rust + REPORT_NAME: report-agntcy-a2a-go-rust + + test:rust-go:jsonrpc:rust-go: + desc: Rust client to Go server JSON-RPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rust-go + REPORT_NAME: report-agntcy-a2a-rust-go + + test:rust-go:jsonrpc:rust-rust: + desc: Rust client to Rust server JSON-RPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rust-rust + REPORT_NAME: report-agntcy-a2a-rust-rust + + test:rust-go:jsonrpc:run: + internal: true cmds: - mkdir -p ./reports - - cd tests && go test . -v -failfast -test.v -test.paniconexit0 -ginkgo.timeout 20m -timeout 20m -ginkgo.v --ginkgo.json-report=../reports/report-agntcy-a2a.json --ginkgo.junit-report=../reports/report-agntcy-a2a.xml + - > + cd tests && + go test . -v -failfast -test.v -test.paniconexit0 + -ginkgo.timeout 20m -timeout 20m -ginkgo.v + {{if .LABEL_FILTER}}-ginkgo.label-filter='{{.LABEL_FILTER}}'{{end}} + --ginkgo.json-report=../reports/{{.REPORT_NAME}}.json + --ginkgo.junit-report=../reports/{{.REPORT_NAME}}.xml default: cmd: task -l \ No newline at end of file diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go index 794bd891..334628a0 100644 --- a/integrations/agntcy-a2a/tests/interop_rust_go_test.go +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -357,7 +357,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func } }) - ginkgo.It("lets the Go client call the Go fixture", func(ctx ginkgo.SpecContext) { + ginkgo.It("lets the Go client call the Go fixture", ginkgo.Label("go-go"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() @@ -373,7 +373,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(streamText).To(gomega.Equal("go server received: ping")) }) - ginkgo.It("lets the Go client call the Rust fixture", func(ctx ginkgo.SpecContext) { + ginkgo.It("lets the Go client call the Rust fixture", ginkgo.Label("go-rust"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() @@ -389,7 +389,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(streamText).To(gomega.Equal("rust server received: ping")) }) - ginkgo.It("lets the Rust client call the Go fixture", func(ctx ginkgo.SpecContext) { + ginkgo.It("lets the Rust client call the Go fixture", ginkgo.Label("rust-go"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() @@ -397,7 +397,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) }) - ginkgo.It("lets the Rust client call the Rust fixture", func(ctx ginkgo.SpecContext) { + ginkgo.It("lets the Rust client call the Rust fixture", ginkgo.Label("rust-rust"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() From 47cabf2af08b4b80e2e297d80d368483b0196723 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 19:07:25 +0200 Subject: [PATCH 06/15] docs: document A2A interoperability suite Signed-off-by: Luca Muscariello --- README.md | 48 +++++++++++++++++++++++++++++-- integrations/agntcy-a2a/README.md | 39 +++++++++++++++++++++---- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 97ad5102..9232bc54 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - [Integration tests](#integration-tests) - [Directory structure](#directory-structure) - [Running tests](#running-tests) + - [A2A interoperability smoke tests](#a2a-interoperability-smoke-tests) - [Running tests using GitHub actions](#running-tests-using-github-actions) - [How to extend tests with your own test](#how-to-extend-tests-with-your-own-test) - [Updating the agntcy/dir testdata](#updating-the-agntcydir-testdata) @@ -31,6 +32,10 @@ csit │   ├── go.sum │   └── Taskfile.yml ├── integrations # Integration tests +│   ├── agntcy-a2a # Integration tests for Rust/Go A2A interoperability +│   │   ├── fixtures +│   │   ├── Taskfile.yml # Tasks for A2A interoperability tests +│   │   └── tests │   ├── agntcy-slim # Integration tests for [agntcy/slim](https://github.com/agntcy/slim) │   │   ├── agentic-apps │   │   ├── Taskfile.yml # Tasks for Slim integration tests @@ -63,6 +68,12 @@ The following tasks are defined: task: Available tasks for this project: * benchmarks:directory:test: All ADS benchmark test * benchmarks:slim:test: All Slim benchmark test +* integrations:a2a:test: All A2A interoperability tests +* integrations:a2a:test:rust-go:jsonrpc: Rust and Go JSON-RPC interoperability smoke test +* integrations:a2a:test:rust-go:jsonrpc:go-go: Go client to Go server JSON-RPC interoperability test +* integrations:a2a:test:rust-go:jsonrpc:go-rust: Go client to Rust server JSON-RPC interoperability test +* integrations:a2a:test:rust-go:jsonrpc:rust-go: Rust client to Go server JSON-RPC interoperability test +* integrations:a2a:test:rust-go:jsonrpc:rust-rust: Rust client to Rust server JSON-RPC interoperability test * integrations:apps:download:wfsm-bin: Get wfsm binary from GitHub * integrations:apps:get-marketing-campaign-cfgs: Populate marketing campaign config file * integrations:apps:init-submodules: Initialize submodules @@ -136,9 +147,12 @@ environment, deploying the components that will be tested, and running the tests ## Running tests We can launch tests using taskfile locally or in GitHub actions. -Running locally we need to create a test cluster and deploy the test env on -it before running the tests. -It requires the following tools to be installed on local machine: +Some suites are self-contained and run directly on the host, while others need +a Kubernetes-based test environment. + +Suites that deploy components on Kubernetes require creating a test cluster and +deploying the test environment before running the tests. +They require the following tools to be installed on the local machine: - [Taskfile](https://taskfile.dev/installation/) - [Go](https://go.dev/doc/install) - [Docker](https://docs.docker.com/get-started/get-docker/) @@ -162,6 +176,34 @@ After we finish the tests we can destroy the test cluster task integratons:kind:destroy ``` +## A2A interoperability smoke tests + +The `integrations/agntcy-a2a` suite is self-contained and does not require a +Kind cluster, Helm, or repository sibling checkouts. It builds and runs small +Go and Rust JSON-RPC fixtures locally to exercise this matrix: + +- Go client -> Go server +- Go client -> Rust server +- Rust client -> Go server +- Rust client -> Rust server + +To run it from the repository root you need: + +- [Taskfile](https://taskfile.dev/installation/) +- [Go](https://go.dev/doc/install) +- [Rust and Cargo](https://www.rust-lang.org/tools/install) + +```bash +task integrations:a2a:test +task integrations:a2a:test:rust-go:jsonrpc +task integrations:a2a:test:rust-go:jsonrpc:go-go +task integrations:a2a:test:rust-go:jsonrpc:go-rust +task integrations:a2a:test:rust-go:jsonrpc:rust-go +task integrations:a2a:test:rust-go:jsonrpc:rust-rust +``` + +The suite writes Ginkgo JSON and JUnit reports under `integrations/agntcy-a2a/reports/`. + ## Running tests using GitHub actions diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index a3ffd052..7f160302 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -11,10 +11,37 @@ The initial slice covers a self-contained Rust and Go JSON-RPC smoke matrix: The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. -Available task targets: +## Matrix -- `task test` or `task test:rust-go:jsonrpc` runs the full matrix. -- `task test:rust-go:jsonrpc:go-go` runs the Go client -> Go server case. -- `task test:rust-go:jsonrpc:go-rust` runs the Go client -> Rust server case. -- `task test:rust-go:jsonrpc:rust-go` runs the Rust client -> Go server case. -- `task test:rust-go:jsonrpc:rust-rust` runs the Rust client -> Rust server case. \ No newline at end of file +| Label | Scenario | Component task | Repository task | +| --- | --- | --- | --- | +| `go-go` | Go client -> Go server | `task test:rust-go:jsonrpc:go-go` | `task integrations:a2a:test:rust-go:jsonrpc:go-go` | +| `go-rust` | Go client -> Rust server | `task test:rust-go:jsonrpc:go-rust` | `task integrations:a2a:test:rust-go:jsonrpc:go-rust` | +| `rust-go` | Rust client -> Go server | `task test:rust-go:jsonrpc:rust-go` | `task integrations:a2a:test:rust-go:jsonrpc:rust-go` | +| `rust-rust` | Rust client -> Rust server | `task test:rust-go:jsonrpc:rust-rust` | `task integrations:a2a:test:rust-go:jsonrpc:rust-rust` | + +## Running The Suite + +From `integrations/agntcy-a2a/`: + +```sh +task test +task test:rust-go:jsonrpc +task test:rust-go:jsonrpc:go-go +task test:rust-go:jsonrpc:go-rust +task test:rust-go:jsonrpc:rust-go +task test:rust-go:jsonrpc:rust-rust +``` + +From the repository root: + +```sh +task integrations:a2a:test +task integrations:a2a:test:rust-go:jsonrpc +task integrations:a2a:test:rust-go:jsonrpc:go-go +task integrations:a2a:test:rust-go:jsonrpc:go-rust +task integrations:a2a:test:rust-go:jsonrpc:rust-go +task integrations:a2a:test:rust-go:jsonrpc:rust-rust +``` + +Each run writes Ginkgo JSON and JUnit reports under `integrations/agntcy-a2a/reports/`. The full-matrix task emits `report-agntcy-a2a.{json,xml}`, and the per-case tasks emit scenario-specific report names. From 0e280452c9d2c50e25ac912fb113f832a17eadad Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sat, 4 Apr 2026 21:46:41 +0200 Subject: [PATCH 07/15] docs: clarify a2a interop task usage Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index 7f160302..045b14d4 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -11,6 +11,8 @@ The initial slice covers a self-contained Rust and Go JSON-RPC smoke matrix: The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. +Each scenario is tagged with a dedicated Ginkgo label and exposed through a matching Task target so the full matrix and each individual leg can be run independently. + ## Matrix | Label | Scenario | Component task | Repository task | @@ -20,7 +22,7 @@ The fixtures are intentionally small and deterministic so the suite can run the | `rust-go` | Rust client -> Go server | `task test:rust-go:jsonrpc:rust-go` | `task integrations:a2a:test:rust-go:jsonrpc:rust-go` | | `rust-rust` | Rust client -> Rust server | `task test:rust-go:jsonrpc:rust-rust` | `task integrations:a2a:test:rust-go:jsonrpc:rust-rust` | -## Running The Suite +## Running the Suite From `integrations/agntcy-a2a/`: @@ -33,6 +35,8 @@ task test:rust-go:jsonrpc:rust-go task test:rust-go:jsonrpc:rust-rust ``` +`task test` is an alias for the full `task test:rust-go:jsonrpc` matrix run. + From the repository root: ```sh @@ -44,4 +48,6 @@ task integrations:a2a:test:rust-go:jsonrpc:rust-go task integrations:a2a:test:rust-go:jsonrpc:rust-rust ``` -Each run writes Ginkgo JSON and JUnit reports under `integrations/agntcy-a2a/reports/`. The full-matrix task emits `report-agntcy-a2a.{json,xml}`, and the per-case tasks emit scenario-specific report names. +`task integrations:a2a:test` is the repository-level alias for the same full matrix run. + +Each run writes Ginkgo JSON and JUnit reports under `integrations/agntcy-a2a/reports/`. The full-matrix task emits `report-agntcy-a2a.{json,xml}`, and the per-case tasks emit scenario-specific report names via `-ginkgo.label-filter`. From 3bbfd4153f112c652e80256f3bab105cb7e14e85 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sun, 5 Apr 2026 11:55:43 +0200 Subject: [PATCH 08/15] test: extend Rust-Go interop matrix Signed-off-by: Luca Muscariello --- .github/workflows/test-integrations.yaml | 2 +- integrations/agntcy-a2a/README.md | 70 ++- integrations/agntcy-a2a/Taskfile.yml | 110 +++- .../fixtures/go-jsonrpc-server/main.go | 166 +++++- .../agntcy-a2a/fixtures/rust/Cargo.lock | 266 ++++++++- .../agntcy-a2a/fixtures/rust/Cargo.toml | 10 +- .../rust/src/bin/interop-rust-probe.rs | 549 +++++++++++++++-- .../rust/src/bin/interop-rust-server.rs | 214 ++++++- .../agntcy-a2a/tests/interop_rust_go_test.go | 563 +++++++++++++++--- integrations/go.mod | 7 +- integrations/go.sum | 34 +- 11 files changed, 1803 insertions(+), 188 deletions(-) diff --git a/.github/workflows/test-integrations.yaml b/.github/workflows/test-integrations.yaml index 45cbaf54..1f570d21 100644 --- a/.github/workflows/test-integrations.yaml +++ b/.github/workflows/test-integrations.yaml @@ -205,7 +205,7 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Run A2A interoperability tests - run: task integrations:a2a:test:rust-go:jsonrpc + run: task integrations:a2a:test:rust-go shell: bash - name: Create artifact from integrations test reports diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index 045b14d4..43273675 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -2,25 +2,47 @@ This component hosts cross-SDK A2A interoperability checks. -The initial slice covers a self-contained Rust and Go JSON-RPC smoke matrix: +The current slice covers Rust and Go across the released JSON-RPC, HTTP+JSON, and gRPC bindings. -- Go client -> Go server -- Go client -> Rust server -- Rust client -> Go server -- Rust client -> Rust server +JSON-RPC and HTTP+JSON are green across the full 4-leg Rust/Go matrix. + +gRPC currently has two passing same-SDK legs plus two explicit incompatibility checks: + +- Go client -> Go server passes with the Go SDK's native bare `host:port` gRPC endpoint form +- Rust client -> Rust server passes with the Rust SDK's native `http://host:port` gRPC endpoint form +- Go client -> Rust server documents that the released Go gRPC client rejects scheme-prefixed endpoints advertised in the Rust card +- Rust client -> Go server documents that the released Rust gRPC client rejects the bare endpoint advertised in the Go card + +Each leg validates the same reusable behavior: + +- unary and streaming `SendMessage` +- lifecycle methods across `GetTask`, `ListTasks`, and `CancelTask` +- negative-path error semantics for missing and non-cancelable tasks +- unsupported push-notification behavior +- preservation of a mixed text plus structured-data request payload and message metadata through task history The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. Each scenario is tagged with a dedicated Ginkgo label and exposed through a matching Task target so the full matrix and each individual leg can be run independently. +The gRPC legs follow the same agent-card discovery path as the other transports: each fixture serves `/.well-known/agent-card.json` over HTTP and advertises a separate gRPC transport endpoint from that card. + ## Matrix -| Label | Scenario | Component task | Repository task | -| --- | --- | --- | --- | -| `go-go` | Go client -> Go server | `task test:rust-go:jsonrpc:go-go` | `task integrations:a2a:test:rust-go:jsonrpc:go-go` | -| `go-rust` | Go client -> Rust server | `task test:rust-go:jsonrpc:go-rust` | `task integrations:a2a:test:rust-go:jsonrpc:go-rust` | -| `rust-go` | Rust client -> Go server | `task test:rust-go:jsonrpc:rust-go` | `task integrations:a2a:test:rust-go:jsonrpc:rust-go` | -| `rust-rust` | Rust client -> Rust server | `task test:rust-go:jsonrpc:rust-rust` | `task integrations:a2a:test:rust-go:jsonrpc:rust-rust` | +| Transport | Label | Scenario | Current outcome | Component task | Repository task | +| --- | --- | --- | --- | --- | --- | +| JSON-RPC | `go-go` | Go client -> Go server | Pass | `task test:rust-go:jsonrpc:go-go` | `task integrations:a2a:test:rust-go:jsonrpc:go-go` | +| JSON-RPC | `go-rust` | Go client -> Rust server | Pass | `task test:rust-go:jsonrpc:go-rust` | `task integrations:a2a:test:rust-go:jsonrpc:go-rust` | +| JSON-RPC | `rust-go` | Rust client -> Go server | Pass | `task test:rust-go:jsonrpc:rust-go` | `task integrations:a2a:test:rust-go:jsonrpc:rust-go` | +| JSON-RPC | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:jsonrpc:rust-rust` | `task integrations:a2a:test:rust-go:jsonrpc:rust-rust` | +| HTTP+JSON | `go-go` | Go client -> Go server | Pass | `task test:rust-go:rest:go-go` | `task integrations:a2a:test:rust-go:rest:go-go` | +| HTTP+JSON | `go-rust` | Go client -> Rust server | Pass | `task test:rust-go:rest:go-rust` | `task integrations:a2a:test:rust-go:rest:go-rust` | +| HTTP+JSON | `rust-go` | Rust client -> Go server | Pass | `task test:rust-go:rest:rust-go` | `task integrations:a2a:test:rust-go:rest:rust-go` | +| HTTP+JSON | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:rest:rust-rust` | `task integrations:a2a:test:rust-go:rest:rust-rust` | +| gRPC | `go-go` | Go client -> Go server | Pass | `task test:rust-go:grpc:go-go` | `task integrations:a2a:test:rust-go:grpc:go-go` | +| gRPC | `go-rust` | Go client -> Rust server | Incompatibility documented | `task test:rust-go:grpc:go-rust` | `task integrations:a2a:test:rust-go:grpc:go-rust` | +| gRPC | `rust-go` | Rust client -> Go server | Incompatibility documented | `task test:rust-go:grpc:rust-go` | `task integrations:a2a:test:rust-go:grpc:rust-go` | +| gRPC | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:grpc:rust-rust` | `task integrations:a2a:test:rust-go:grpc:rust-rust` | ## Running the Suite @@ -28,26 +50,48 @@ From `integrations/agntcy-a2a/`: ```sh task test +task test:rust-go task test:rust-go:jsonrpc +task test:rust-go:rest +task test:rust-go:grpc task test:rust-go:jsonrpc:go-go task test:rust-go:jsonrpc:go-rust task test:rust-go:jsonrpc:rust-go task test:rust-go:jsonrpc:rust-rust +task test:rust-go:rest:go-go +task test:rust-go:rest:go-rust +task test:rust-go:rest:rust-go +task test:rust-go:rest:rust-rust +task test:rust-go:grpc:go-go +task test:rust-go:grpc:go-rust +task test:rust-go:grpc:rust-go +task test:rust-go:grpc:rust-rust ``` -`task test` is an alias for the full `task test:rust-go:jsonrpc` matrix run. +`task test` is an alias for the full `task test:rust-go` transport matrix run. From the repository root: ```sh task integrations:a2a:test +task integrations:a2a:test:rust-go task integrations:a2a:test:rust-go:jsonrpc +task integrations:a2a:test:rust-go:rest +task integrations:a2a:test:rust-go:grpc task integrations:a2a:test:rust-go:jsonrpc:go-go task integrations:a2a:test:rust-go:jsonrpc:go-rust task integrations:a2a:test:rust-go:jsonrpc:rust-go task integrations:a2a:test:rust-go:jsonrpc:rust-rust +task integrations:a2a:test:rust-go:rest:go-go +task integrations:a2a:test:rust-go:rest:go-rust +task integrations:a2a:test:rust-go:rest:rust-go +task integrations:a2a:test:rust-go:rest:rust-rust +task integrations:a2a:test:rust-go:grpc:go-go +task integrations:a2a:test:rust-go:grpc:go-rust +task integrations:a2a:test:rust-go:grpc:rust-go +task integrations:a2a:test:rust-go:grpc:rust-rust ``` `task integrations:a2a:test` is the repository-level alias for the same full matrix run. -Each run writes Ginkgo JSON and JUnit reports under `integrations/agntcy-a2a/reports/`. The full-matrix task emits `report-agntcy-a2a.{json,xml}`, and the per-case tasks emit scenario-specific report names via `-ginkgo.label-filter`. +Each run writes Ginkgo JSON and JUnit reports under `integrations/agntcy-a2a/reports/`. The full transport matrix emits `report-agntcy-a2a.{json,xml}`, the transport-scoped tasks emit `report-agntcy-a2a-jsonrpc.{json,xml}`, `report-agntcy-a2a-rest.{json,xml}`, and `report-agntcy-a2a-grpc.{json,xml}`, and the per-case tasks emit scenario-specific report names via `-ginkgo.label-filter`. diff --git a/integrations/agntcy-a2a/Taskfile.yml b/integrations/agntcy-a2a/Taskfile.yml index b0410688..5ebb225e 100644 --- a/integrations/agntcy-a2a/Taskfile.yml +++ b/integrations/agntcy-a2a/Taskfile.yml @@ -10,21 +10,45 @@ tasks: test: desc: All A2A interoperability tests cmds: - - task: test:rust-go:jsonrpc + - task: test:rust-go + + test:rust-go: + desc: Rust and Go interoperability test matrix across JSON-RPC, HTTP+JSON, and gRPC + cmds: + - task: test:rust-go:run + vars: + REPORT_NAME: report-agntcy-a2a test:rust-go:jsonrpc: - desc: Rust and Go JSON-RPC interoperability smoke test + desc: Rust and Go JSON-RPC interoperability matrix cmds: - task: test:rust-go:jsonrpc:run vars: - REPORT_NAME: report-agntcy-a2a + LABEL_FILTER: jsonrpc + REPORT_NAME: report-agntcy-a2a-jsonrpc + + test:rust-go:rest: + desc: Rust and Go HTTP+JSON interoperability matrix + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rest + REPORT_NAME: report-agntcy-a2a-rest + + test:rust-go:grpc: + desc: Rust and Go gRPC interoperability matrix + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: grpc + REPORT_NAME: report-agntcy-a2a-grpc test:rust-go:jsonrpc:go-go: desc: Go client to Go server JSON-RPC interoperability test cmds: - task: test:rust-go:jsonrpc:run vars: - LABEL_FILTER: go-go + LABEL_FILTER: jsonrpc && go-go REPORT_NAME: report-agntcy-a2a-go-go test:rust-go:jsonrpc:go-rust: @@ -32,7 +56,7 @@ tasks: cmds: - task: test:rust-go:jsonrpc:run vars: - LABEL_FILTER: go-rust + LABEL_FILTER: jsonrpc && go-rust REPORT_NAME: report-agntcy-a2a-go-rust test:rust-go:jsonrpc:rust-go: @@ -40,7 +64,7 @@ tasks: cmds: - task: test:rust-go:jsonrpc:run vars: - LABEL_FILTER: rust-go + LABEL_FILTER: jsonrpc && rust-go REPORT_NAME: report-agntcy-a2a-rust-go test:rust-go:jsonrpc:rust-rust: @@ -48,9 +72,73 @@ tasks: cmds: - task: test:rust-go:jsonrpc:run vars: - LABEL_FILTER: rust-rust + LABEL_FILTER: jsonrpc && rust-rust REPORT_NAME: report-agntcy-a2a-rust-rust + test:rust-go:rest:go-go: + desc: Go client to Go server HTTP+JSON interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rest && go-go + REPORT_NAME: report-agntcy-a2a-rest-go-go + + test:rust-go:rest:go-rust: + desc: Go client to Rust server HTTP+JSON interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rest && go-rust + REPORT_NAME: report-agntcy-a2a-rest-go-rust + + test:rust-go:rest:rust-go: + desc: Rust client to Go server HTTP+JSON interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rest && rust-go + REPORT_NAME: report-agntcy-a2a-rest-rust-go + + test:rust-go:rest:rust-rust: + desc: Rust client to Rust server HTTP+JSON interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: rest && rust-rust + REPORT_NAME: report-agntcy-a2a-rest-rust-rust + + test:rust-go:grpc:go-go: + desc: Go client to Go server gRPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: grpc && go-go + REPORT_NAME: report-agntcy-a2a-grpc-go-go + + test:rust-go:grpc:go-rust: + desc: Go client to Rust server gRPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: grpc && go-rust + REPORT_NAME: report-agntcy-a2a-grpc-go-rust + + test:rust-go:grpc:rust-go: + desc: Rust client to Go server gRPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: grpc && rust-go + REPORT_NAME: report-agntcy-a2a-grpc-rust-go + + test:rust-go:grpc:rust-rust: + desc: Rust client to Rust server gRPC interoperability test + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: grpc && rust-rust + REPORT_NAME: report-agntcy-a2a-grpc-rust-rust + test:rust-go:jsonrpc:run: internal: true cmds: @@ -63,5 +151,13 @@ tasks: --ginkgo.json-report=../reports/{{.REPORT_NAME}}.json --ginkgo.junit-report=../reports/{{.REPORT_NAME}}.xml + test:rust-go:run: + internal: true + cmds: + - task: test:rust-go:jsonrpc:run + vars: + LABEL_FILTER: '{{.LABEL_FILTER}}' + REPORT_NAME: '{{.REPORT_NAME}}' + default: cmd: task -l \ No newline at end of file diff --git a/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go b/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go index 12aa3666..01b3841d 100644 --- a/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go +++ b/integrations/agntcy-a2a/fixtures/go-jsonrpc-server/main.go @@ -13,24 +13,67 @@ import ( "net/http" "github.com/a2aproject/a2a-go/v2/a2a" + a2agrpc "github.com/a2aproject/a2a-go/v2/a2agrpc/v1" "github.com/a2aproject/a2a-go/v2/a2asrv" + "github.com/a2aproject/a2a-go/v2/a2asrv/taskstore" + "google.golang.org/grpc" +) + +type transportProtocol string + +const ( + pendingRequestText = "pending" + transportProtocolJSONRPC transportProtocol = "jsonrpc" + transportProtocolREST transportProtocol = "rest" + transportProtocolGRPC transportProtocol = "grpc" ) type interopExecutor struct{} +func responseText(message *a2a.Message) string { + return fmt.Sprintf("go server received: %s", firstText(message)) +} + +func buildTask(execCtx *a2asrv.ExecutorContext, state a2a.TaskState, text string) *a2a.Task { + task := a2a.NewSubmittedTask(execCtx, execCtx.Message) + task.Status = a2a.TaskStatus{ + State: state, + Message: a2a.NewMessageForTask( + a2a.MessageRoleAgent, + task, + a2a.NewTextPart(text), + ), + } + return task +} + func (*interopExecutor) Execute(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { - response := a2a.NewMessage( - a2a.MessageRoleAgent, - a2a.NewTextPart(fmt.Sprintf("go server received: %s", firstText(execCtx.Message))), - ) + state := a2a.TaskStateCompleted + if firstText(execCtx.Message) == pendingRequestText { + state = a2a.TaskStateWorking + } + response := buildTask(execCtx, state, responseText(execCtx.Message)) return func(yield func(a2a.Event, error) bool) { yield(response, nil) } } -func (*interopExecutor) Cancel(_ context.Context, _ *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { - return func(yield func(a2a.Event, error) bool) {} +func (*interopExecutor) Cancel(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] { + return func(yield func(a2a.Event, error) bool) { + yield( + a2a.NewStatusUpdateEvent( + execCtx, + a2a.TaskStateCanceled, + a2a.NewMessageForTask( + a2a.MessageRoleAgent, + execCtx, + a2a.NewTextPart("go server canceled task"), + ), + ), + nil, + ) + } } func firstText(message *a2a.Message) string { @@ -47,15 +90,41 @@ func firstText(message *a2a.Message) string { return "" } -func agentCard(port int) *a2a.AgentCard { - baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) +func parseTransportProtocol(raw string) (transportProtocol, error) { + switch transportProtocol(raw) { + case transportProtocolJSONRPC: + return transportProtocolJSONRPC, nil + case transportProtocolREST: + return transportProtocolREST, nil + case transportProtocolGRPC: + return transportProtocolGRPC, nil + default: + return "", fmt.Errorf("unsupported protocol %q", raw) + } +} + +func agentCard(cardPort int, protocol transportProtocol, grpcPort int) *a2a.AgentCard { + baseURL := fmt.Sprintf("http://127.0.0.1:%d", cardPort) + name := "CSIT Go JSON-RPC Agent" + iface := a2a.NewAgentInterface(baseURL+"/rpc", a2a.TransportProtocolJSONRPC) + + if protocol == transportProtocolREST { + name = "CSIT Go REST Agent" + iface = a2a.NewAgentInterface(baseURL, a2a.TransportProtocolHTTPJSON) + } else if protocol == transportProtocolGRPC { + name = "CSIT Go gRPC Agent" + iface = a2a.NewAgentInterface( + fmt.Sprintf("127.0.0.1:%d", grpcPort), + a2a.TransportProtocolGRPC, + ) + } return &a2a.AgentCard{ - Name: "CSIT Go JSON-RPC Agent", + Name: name, Description: "Go interoperability fixture for CSIT", Version: "1.0.0", SupportedInterfaces: []*a2a.AgentInterface{ - a2a.NewAgentInterface(baseURL+"/rpc", a2a.TransportProtocolJSONRPC), + iface, }, Capabilities: a2a.AgentCapabilities{Streaming: true}, DefaultInputModes: []string{"text/plain"}, @@ -63,21 +132,88 @@ func agentCard(port int) *a2a.AgentCard { } } +func serveAgentCard(listener net.Listener, card *a2a.AgentCard) error { + mux := http.NewServeMux() + mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(card)) + return http.Serve(listener, mux) +} + +func serveGRPC(listener net.Listener, handler a2asrv.RequestHandler) error { + grpcHandler := a2agrpc.NewHandler(handler) + server := grpc.NewServer() + grpcHandler.RegisterWith(server) + return server.Serve(listener) +} + func main() { - port := flag.Int("port", 19091, "port for the JSON-RPC fixture server") + port := flag.Int("port", 19091, "port for the fixture agent card server") + grpcPort := flag.Int("grpc-port", 19092, "port for the gRPC fixture transport server") + protocolFlag := flag.String("protocol", string(transportProtocolJSONRPC), "transport protocol to serve: jsonrpc, rest, or grpc") flag.Parse() + protocol, err := parseTransportProtocol(*protocolFlag) + if err != nil { + log.Fatalf("failed to parse protocol: %v", err) + } + + handler := a2asrv.NewHandler( + &interopExecutor{}, + a2asrv.WithTaskStore(taskstore.NewInMemory(&taskstore.InMemoryStoreConfig{ + Authenticator: func(context.Context) (string, error) { + return "csit-user", nil + }, + })), + ) + + if protocol == transportProtocolGRPC { + cardListener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port)) + if err != nil { + log.Fatalf("failed to bind agent card listener: %v", err) + } + + transportListener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *grpcPort)) + if err != nil { + log.Fatalf("failed to bind gRPC listener: %v", err) + } + + card := agentCard(*port, protocol, *grpcPort) + errCh := make(chan error, 2) + + go func() { + errCh <- serveGRPC(transportListener, handler) + }() + go func() { + errCh <- serveAgentCard(cardListener, card) + }() + + log.Printf( + "go grpc fixture listening on http://127.0.0.1:%d with card http://127.0.0.1:%d", + *grpcPort, + *port, + ) + + if err := <-errCh; err != nil { + log.Fatalf("server stopped: %v", err) + } + return + } + listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port)) if err != nil { log.Fatalf("failed to bind listener: %v", err) } - handler := a2asrv.NewHandler(&interopExecutor{}) mux := http.NewServeMux() - mux.Handle("/rpc", a2asrv.NewJSONRPCHandler(handler)) - mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(agentCard(*port))) + switch protocol { + case transportProtocolJSONRPC: + mux.Handle("/rpc", a2asrv.NewJSONRPCHandler(handler)) + log.Printf("go jsonrpc fixture listening on http://127.0.0.1:%d", *port) + case transportProtocolREST: + mux.Handle("/", a2asrv.NewRESTHandler(handler)) + log.Printf("go rest fixture listening on http://127.0.0.1:%d", *port) + } + mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(agentCard(*port, protocol, *grpcPort))) - log.Printf("go jsonrpc fixture listening on http://127.0.0.1:%d", *port) if err := http.Serve(listener, mux); err != nil { log.Fatalf("server stopped: %v", err) } diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index be74c793..19b55546 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-client" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea81781417ec53efef6249b8bd76fb88d4c5f3078247cf28d7af95365bc41cf" +checksum = "0c311e723bc040d93f1382a845caf4ee5504c6049d83baf8ec42ff36001e737b" dependencies = [ "agntcy-a2a", "async-trait", @@ -43,17 +43,55 @@ version = "0.1.0" dependencies = [ "agntcy-a2a", "agntcy-a2a-client", + "agntcy-a2a-grpc", + "agntcy-a2a-pb", "agntcy-a2a-server", "axum", "futures", + "serde_json", "tokio", + "tonic", ] [[package]] -name = "agntcy-a2a-server" +name = "agntcy-a2a-grpc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b470f3792b40c58ce0d957c2263a397f7a34508afa84755ff5357745228f8afe" +dependencies = [ + "agntcy-a2a", + "agntcy-a2a-client", + "agntcy-a2a-pb", + "agntcy-a2a-server", + "async-trait", + "futures", + "prost", + "tokio", + "tokio-stream", + "tonic", + "tracing", +] + +[[package]] +name = "agntcy-a2a-pb" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b154eaf616b336d9dcf5a7c7c8ff42a7da1c3f0ebee72e19216ed06245d54f" +checksum = "091a02c81d4f9e136ed454860dd8a869cb47cf685a37b8ae5175216e29b87cb6" +dependencies = [ + "agntcy-a2a", + "chrono", + "prost", + "prost-types", + "serde_json", + "tonic", + "tonic-build", +] + +[[package]] +name = "agntcy-a2a-server" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1564f813121e70fc563b8fb013c3e2be5bdffd37049384bcbfbd348276e834c" dependencies = [ "agntcy-a2a", "async-trait", @@ -72,6 +110,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -265,6 +312,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -302,6 +355,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "fnv" version = "1.0.7" @@ -573,6 +632,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -606,7 +678,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -775,6 +847,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -861,6 +942,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "native-tls" version = "0.2.18" @@ -966,6 +1053,36 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1006,6 +1123,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.45" @@ -1030,6 +1199,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "reqwest" version = "0.12.28" @@ -1283,6 +1481,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -1412,7 +1620,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -1472,6 +1680,49 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + [[package]] name = "tower" version = "0.5.3" @@ -1480,9 +1731,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index dd053347..f395fb95 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -6,8 +6,12 @@ publish = false [dependencies] a2a = { package = "agntcy-a2a", version = "0.2.3" } -a2a-client = { package = "agntcy-a2a-client", version = "0.1.6" } -a2a-server = { package = "agntcy-a2a-server", version = "0.1.3" } +a2a-client = { package = "agntcy-a2a-client", version = "0.1.7" } +a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.4" } +a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.3" } +a2a-server = { package = "agntcy-a2a-server", version = "0.2.0" } axum = "0.8" futures = "0.3" -tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] } \ No newline at end of file +serde_json = "1" +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] } +tonic = { version = "0.13", features = ["transport"] } \ No newline at end of file diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs index 06d2ec4c..2276c0f4 100644 --- a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs @@ -3,21 +3,40 @@ use std::env; use std::process; +use std::sync::Arc; use a2a::*; use a2a_client::A2AClientFactory; use a2a_client::agent_card::AgentCardResolver; +use a2a_grpc::GrpcTransportFactory; use futures::StreamExt; +use futures::stream::BoxStream; +use serde_json::{Value, json}; + +const REQUEST_TEXT: &str = "ping"; +const PENDING_REQUEST_TEXT: &str = "pending"; +const REQUEST_DATA_KIND: &str = "structured"; +const REQUEST_DATA_SCOPE: &str = "interop"; +const REQUEST_METADATA_KEY: &str = "csit"; +const REQUEST_METADATA_VALUE: &str = "multipart"; struct Args { card_url: String, - expect_text: String, + server_prefix: String, + expect_subscribe_unsupported: bool, + expect_push_unsupported: bool, + relaxed_error_checks: bool, + expected_push_error_code: i32, } fn parse_args() -> Result { let mut args = env::args().skip(1); let mut card_url = None; - let mut expect_text = None; + let mut server_prefix = None; + let mut expect_subscribe_unsupported = false; + let mut expect_push_unsupported = false; + let mut relaxed_error_checks = false; + let mut expected_push_error_code = a2a::error_code::PUSH_NOTIFICATION_NOT_SUPPORTED; while let Some(arg) = args.next() { match arg.as_str() { @@ -27,12 +46,29 @@ fn parse_args() -> Result { .ok_or_else(|| "--card-url requires a value".to_string())?, ); } - "--expect-text" => { - expect_text = Some( + "--server-prefix" => { + server_prefix = Some( args.next() - .ok_or_else(|| "--expect-text requires a value".to_string())?, + .ok_or_else(|| "--server-prefix requires a value".to_string())?, ); } + "--expect-subscribe-unsupported" => { + expect_subscribe_unsupported = true; + } + "--expect-push-unsupported" => { + expect_push_unsupported = true; + } + "--expected-push-error-code" => { + let value = args + .next() + .ok_or_else(|| "--expected-push-error-code requires a value".to_string())?; + expected_push_error_code = value + .parse::() + .map_err(|_| "--expected-push-error-code must be a valid i32".to_string())?; + } + "--relaxed-error-checks" => { + relaxed_error_checks = true; + } other => { return Err(format!("unknown argument: {other}")); } @@ -41,7 +77,11 @@ fn parse_args() -> Result { Ok(Args { card_url: card_url.ok_or_else(|| "missing --card-url".to_string())?, - expect_text: expect_text.ok_or_else(|| "missing --expect-text".to_string())?, + server_prefix: server_prefix.ok_or_else(|| "missing --server-prefix".to_string())?, + expect_subscribe_unsupported, + expect_push_unsupported, + relaxed_error_checks, + expected_push_error_code, }) } @@ -64,31 +104,192 @@ fn assert_text(actual: String, expected: &str, kind: &str) -> Result<(), String> } } -fn unary_text(response: SendMessageResponse) -> Result { +fn assert_state(actual: &TaskState, expected: TaskState, kind: &str) -> Result<(), String> { + if *actual == expected { + Ok(()) + } else { + Err(format!( + "unexpected {kind} task state: got {actual:?}, want {expected:?}" + )) + } +} + +fn assert_error_code( + result: Result, + expected_code: i32, + kind: &str, +) -> Result<(), String> { + match result { + Ok(_) => Err(format!( + "expected {kind} to fail with code {expected_code}, but it succeeded" + )), + Err(error) if error.code == expected_code => Ok(()), + Err(error) => Err(format!( + "unexpected {kind} error code: got {}, want {} ({error})", + error.code, expected_code + )), + } +} + +fn assert_failed(result: Result, kind: &str) -> Result<(), String> { + match result { + Ok(_) => Err(format!("expected {kind} to fail, but it succeeded")), + Err(_) => Ok(()), + } +} + +async fn assert_stream_error_code( + result: Result>, A2AError>, + expected_code: i32, + kind: &str, +) -> Result<(), String> { + match result { + Err(error) if error.code == expected_code => Ok(()), + Err(error) => Err(format!( + "unexpected {kind} error code: got {}, want {} ({error})", + error.code, expected_code + )), + Ok(mut stream) => match stream.next().await { + Some(Err(error)) if error.code == expected_code => Ok(()), + Some(Err(error)) => Err(format!( + "unexpected {kind} stream error code: got {}, want {} ({error})", + error.code, expected_code + )), + Some(Ok(_)) => Err(format!( + "expected {kind} to fail with code {expected_code}, but it yielded an event" + )), + None => Err(format!( + "expected {kind} to fail with code {expected_code}, but the stream ended cleanly" + )), + }, + } +} + +fn expected_response_text(server_prefix: &str, request_text: &str) -> String { + format!("{server_prefix} server received: {request_text}") +} + +fn expected_cancel_text(server_prefix: &str) -> String { + format!("{server_prefix} server canceled task") +} + +fn request_with_payload(text: &str, return_immediately: bool) -> SendMessageRequest { + let mut message = Message::new( + Role::User, + vec![ + Part::text(text), + Part::data(json!({ + "kind": REQUEST_DATA_KIND, + "scope": REQUEST_DATA_SCOPE, + })), + ], + ); + message.metadata = Some(std::collections::HashMap::from([( + REQUEST_METADATA_KEY.to_string(), + json!(REQUEST_METADATA_VALUE), + )])); + + SendMessageRequest { + message, + configuration: Some(SendMessageConfiguration { + accepted_output_modes: None, + push_notification_config: None, + history_length: None, + return_immediately: Some(return_immediately), + }), + metadata: None, + tenant: None, + } +} + +fn task_text(task: &Task) -> Result { + task.status + .message + .as_ref() + .ok_or_else(|| "task response contained no message".to_string()) + .and_then(first_text) +} + +fn task_from_response(response: SendMessageResponse, kind: &str) -> Result { match response { - SendMessageResponse::Message(message) => first_text(&message), - SendMessageResponse::Task(task) => task - .status - .message - .as_ref() - .ok_or_else(|| "task response contained no message".to_string()) - .and_then(first_text), + SendMessageResponse::Task(task) => Ok(task), + SendMessageResponse::Message(_) => Err(format!("unexpected {kind} response type: Message")), } } -fn streaming_text(response: StreamResponse) -> Result { +fn stream_response_text(response: StreamResponse) -> Result, String> { match response { - StreamResponse::Message(message) => first_text(&message), - StreamResponse::Task(task) => task - .status - .message - .as_ref() - .ok_or_else(|| "stream task event contained no message".to_string()) - .and_then(first_text), - other => Err(format!("unexpected streaming event: {other:?}")), + StreamResponse::Message(message) => first_text(&message).map(Some), + StreamResponse::Task(task) => task_text(&task).map(Some), + StreamResponse::StatusUpdate(update) => { + update.status.message.as_ref().map(first_text).transpose() + } + StreamResponse::ArtifactUpdate(_) => Ok(None), } } +fn assert_task_history(task: &Task, expected_text: &str, kind: &str) -> Result<(), String> { + let history = task + .history + .as_ref() + .ok_or_else(|| format!("{kind} task did not include history"))?; + let message = history + .last() + .ok_or_else(|| format!("{kind} task history was empty"))?; + + if message.parts.len() != 2 { + return Err(format!( + "{kind} task history had {} parts, want 2", + message.parts.len() + )); + } + + let actual = first_text(message)?; + assert_text(actual, expected_text, kind)?; + + let part_data = match &message.parts[1].content { + PartContent::Data(value) => value, + _ => { + return Err(format!( + "{kind} task history second part was not a structured data part" + )); + } + }; + let kind_value = part_data + .get("kind") + .and_then(Value::as_str) + .ok_or_else(|| format!("{kind} task history data part was missing kind"))?; + let scope_value = part_data + .get("scope") + .and_then(Value::as_str) + .ok_or_else(|| format!("{kind} task history data part was missing scope"))?; + + if kind_value != REQUEST_DATA_KIND || scope_value != REQUEST_DATA_SCOPE { + return Err(format!( + "{kind} task history data part mismatch: got kind={kind_value:?} scope={scope_value:?}" + )); + } + + let metadata = message + .metadata + .as_ref() + .ok_or_else(|| format!("{kind} task history message was missing metadata"))?; + let metadata_value = metadata + .get(REQUEST_METADATA_KEY) + .and_then(Value::as_str) + .ok_or_else(|| { + format!("{kind} task history metadata was missing {REQUEST_METADATA_KEY}") + })?; + + if metadata_value != REQUEST_METADATA_VALUE { + return Err(format!( + "{kind} task history metadata mismatch: got {metadata_value:?}, want {REQUEST_METADATA_VALUE:?}" + )); + } + + Ok(()) +} + #[tokio::main] async fn main() { let args = match parse_args() { @@ -112,34 +313,79 @@ async fn run(args: Args) -> Result<(), String> { .await .map_err(|error| format!("agent card resolution failed: {error}"))?; let client = A2AClientFactory::builder() + .register(Arc::new(GrpcTransportFactory)) + .preferred_bindings(vec![ + TRANSPORT_PROTOCOL_GRPC.to_string(), + TRANSPORT_PROTOCOL_JSONRPC.to_string(), + TRANSPORT_PROTOCOL_HTTP_JSON.to_string(), + ]) .build() .create_from_card(&card) .await .map_err(|error| format!("client creation failed: {error}"))?; - let request = SendMessageRequest { - message: Message::new(Role::User, vec![Part::text("ping")]), - configuration: None, - metadata: None, - tenant: None, - }; + let expected_ping_text = expected_response_text(&args.server_prefix, REQUEST_TEXT); + let expected_pending_text = expected_response_text(&args.server_prefix, PENDING_REQUEST_TEXT); + let expected_cancel_text = expected_cancel_text(&args.server_prefix); + + let request = request_with_payload(REQUEST_TEXT, false); let response = client .send_message(&request) .await .map_err(|error| format!("unary request failed: {error}"))?; - assert_text(unary_text(response)?, &args.expect_text, "unary")?; + let completed_task = task_from_response(response, "unary")?; + assert_state(&completed_task.status.state, TaskState::Completed, "unary")?; + assert_text(task_text(&completed_task)?, &expected_ping_text, "unary")?; + assert_task_history(&completed_task, REQUEST_TEXT, "unary")?; + + let fetched_task = client + .get_task(&GetTaskRequest { + id: completed_task.id.clone(), + history_length: Some(1), + tenant: None, + }) + .await + .map_err(|error| format!("get_task failed: {error}"))?; + assert_state(&fetched_task.status.state, TaskState::Completed, "get_task")?; + assert_text(task_text(&fetched_task)?, &expected_ping_text, "get_task")?; + assert_task_history(&fetched_task, REQUEST_TEXT, "get_task")?; + + let listed_tasks = client + .list_tasks(&ListTasksRequest { + context_id: Some(completed_task.context_id.clone()), + status: None, + page_size: None, + page_token: None, + history_length: None, + status_timestamp_after: None, + include_artifacts: None, + tenant: None, + }) + .await + .map_err(|error| format!("list_tasks failed: {error}"))?; + if !listed_tasks + .tasks + .iter() + .any(|task| task.id == completed_task.id) + { + return Err(format!( + "list_tasks did not include expected task {}", + completed_task.id + )); + } let mut stream = client .send_streaming_message(&request) .await .map_err(|error| format!("streaming request failed: {error}"))?; - let stream_event = loop { + let streaming_text = loop { match stream.next().await { - Some(Ok(StreamResponse::StatusUpdate(_))) => continue, - Some(Ok(StreamResponse::ArtifactUpdate(_))) => continue, - Some(Ok(event)) => break event, + Some(Ok(event)) => match stream_response_text(event)? { + Some(text) => break text, + None => continue, + }, Some(Err(error)) => { return Err(format!("streaming event failed: {error}")); } @@ -148,12 +394,239 @@ async fn run(args: Args) -> Result<(), String> { } } }; + assert_text(streaming_text, &expected_ping_text, "streaming")?; + + let pending_task = task_from_response( + client + .send_message(&request_with_payload(PENDING_REQUEST_TEXT, true)) + .await + .map_err(|error| format!("pending unary request failed: {error}"))?, + "pending unary", + )?; + assert_state( + &pending_task.status.state, + TaskState::Working, + "pending unary", + )?; + assert_text( + task_text(&pending_task)?, + &expected_pending_text, + "pending unary", + )?; + + let canceled_task = client + .cancel_task(&CancelTaskRequest { + id: pending_task.id.clone(), + metadata: None, + tenant: None, + }) + .await + .map_err(|error| format!("cancel_task failed: {error}"))?; + assert_state( + &canceled_task.status.state, + TaskState::Canceled, + "cancel_task", + )?; assert_text( - streaming_text(stream_event)?, - &args.expect_text, - "streaming", + task_text(&canceled_task)?, + &expected_cancel_text, + "cancel_task", )?; - println!("validated {0} against {1}", args.expect_text, args.card_url); + let fetched_canceled_task = client + .get_task(&GetTaskRequest { + id: pending_task.id.clone(), + history_length: None, + tenant: None, + }) + .await + .map_err(|error| format!("get_task after cancel failed: {error}"))?; + assert_state( + &fetched_canceled_task.status.state, + TaskState::Canceled, + "get_task after cancel", + )?; + assert_text( + task_text(&fetched_canceled_task)?, + &expected_cancel_text, + "get_task after cancel", + )?; + + if args.relaxed_error_checks { + assert_failed( + client + .get_task(&GetTaskRequest { + id: new_task_id(), + history_length: None, + tenant: None, + }) + .await, + "get missing task", + )?; + + assert_failed( + client + .cancel_task(&CancelTaskRequest { + id: completed_task.id.clone(), + metadata: None, + tenant: None, + }) + .await, + "cancel completed task", + )?; + } else { + assert_error_code( + client + .get_task(&GetTaskRequest { + id: new_task_id(), + history_length: None, + tenant: None, + }) + .await, + a2a::error_code::TASK_NOT_FOUND, + "get missing task", + )?; + + assert_error_code( + client + .cancel_task(&CancelTaskRequest { + id: completed_task.id.clone(), + metadata: None, + tenant: None, + }) + .await, + a2a::error_code::TASK_NOT_CANCELABLE, + "cancel completed task", + )?; + } + + if args.expect_subscribe_unsupported { + assert_stream_error_code( + client + .subscribe_to_task(&SubscribeToTaskRequest { + id: completed_task.id.clone(), + tenant: None, + }) + .await, + a2a::error_code::UNSUPPORTED_OPERATION, + "subscribe_to_task", + ) + .await?; + } + + if args.expect_push_unsupported { + let push_config = PushNotificationConfig { + url: "https://example.invalid/webhook".to_string(), + id: Some("interop-config".to_string()), + token: None, + authentication: None, + }; + + if args.relaxed_error_checks { + assert_failed( + client + .create_push_config(&CreateTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + config: push_config.clone(), + tenant: None, + }) + .await, + "create_push_config", + )?; + + assert_failed( + client + .get_push_config(&GetTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + id: "interop-config".to_string(), + tenant: None, + }) + .await, + "get_push_config", + )?; + + assert_failed( + client + .list_push_configs(&ListTaskPushNotificationConfigsRequest { + task_id: completed_task.id.clone(), + page_size: None, + page_token: None, + tenant: None, + }) + .await, + "list_push_configs", + )?; + + assert_failed( + client + .delete_push_config(&DeleteTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + id: "interop-config".to_string(), + tenant: None, + }) + .await, + "delete_push_config", + )?; + } else { + assert_error_code( + client + .create_push_config(&CreateTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + config: push_config.clone(), + tenant: None, + }) + .await, + args.expected_push_error_code, + "create_push_config", + )?; + + assert_error_code( + client + .get_push_config(&GetTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + id: "interop-config".to_string(), + tenant: None, + }) + .await, + args.expected_push_error_code, + "get_push_config", + )?; + + assert_error_code( + client + .list_push_configs(&ListTaskPushNotificationConfigsRequest { + task_id: completed_task.id.clone(), + page_size: None, + page_token: None, + tenant: None, + }) + .await, + args.expected_push_error_code, + "list_push_configs", + )?; + + assert_error_code( + client + .delete_push_config(&DeleteTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + id: "interop-config".to_string(), + tenant: None, + }) + .await, + args.expected_push_error_code, + "delete_push_config", + )?; + } + } + + let protocol = card + .supported_interfaces + .first() + .map(|iface| iface.protocol_binding.clone()) + .unwrap_or_else(|| "unknown".to_string()); + println!( + "validated {} {} lifecycle against {}", + args.server_prefix, protocol, args.card_url + ); Ok(()) } diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs index a43da507..a25473d9 100644 --- a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs @@ -5,10 +5,41 @@ use std::env; use std::sync::Arc; use a2a::*; +use a2a_grpc::GrpcHandler; +use a2a_pb::proto::a2a_service_server::A2aServiceServer; use a2a_server::{DefaultRequestHandler, InMemoryTaskStore, StaticAgentCard}; use axum::Router; use futures::stream::{self, BoxStream}; use tokio::net::TcpListener; +use tonic::transport::Server; + +const PENDING_REQUEST_TEXT: &str = "pending"; + +#[derive(Clone, Copy)] +enum TransportProtocol { + JsonRpc, + Rest, + Grpc, +} + +impl TransportProtocol { + fn from_str(value: &str) -> Self { + match value { + "jsonrpc" => Self::JsonRpc, + "rest" => Self::Rest, + "grpc" => Self::Grpc, + other => panic!("unsupported protocol: {other}"), + } + } + + fn name(self) -> &'static str { + match self { + Self::JsonRpc => "jsonrpc", + Self::Rest => "rest", + Self::Grpc => "grpc", + } + } +} struct InteropExecutor; @@ -19,17 +50,38 @@ fn first_text(message: Option<&Message>) -> String { .to_string() } -fn build_agent_card(port: u16) -> AgentCard { - let base_url = format!("http://127.0.0.1:{port}"); +fn build_agent_card( + card_port: u16, + protocol: TransportProtocol, + grpc_port: Option, +) -> AgentCard { + let base_url = format!("http://127.0.0.1:{card_port}"); + let (name, interface_url, binding) = match protocol { + TransportProtocol::JsonRpc => ( + "CSIT Rust JSON-RPC Agent", + format!("{base_url}/rpc"), + TRANSPORT_PROTOCOL_JSONRPC, + ), + TransportProtocol::Rest => ( + "CSIT Rust REST Agent", + base_url.clone(), + TRANSPORT_PROTOCOL_HTTP_JSON, + ), + TransportProtocol::Grpc => ( + "CSIT Rust gRPC Agent", + format!( + "http://127.0.0.1:{}", + grpc_port.expect("gRPC transport requires a dedicated port") + ), + TRANSPORT_PROTOCOL_GRPC, + ), + }; AgentCard { - name: "CSIT Rust JSON-RPC Agent".to_string(), + name: name.to_string(), description: "Rust interoperability fixture for CSIT".to_string(), version: VERSION.to_string(), - supported_interfaces: vec![AgentInterface::new( - format!("{base_url}/rpc"), - TRANSPORT_PROTOCOL_JSONRPC, - )], + supported_interfaces: vec![AgentInterface::new(interface_url, binding)], capabilities: AgentCapabilities { streaming: Some(true), push_notifications: Some(false), @@ -58,53 +110,173 @@ fn build_response_message(request: Option<&Message>) -> Message { ) } +fn build_task_message(task_id: &str, context_id: &str, text: impl Into) -> Message { + let mut message = Message::new(Role::Agent, vec![Part::text(text)]); + message.task_id = Some(task_id.to_string()); + message.context_id = Some(context_id.to_string()); + message +} + +fn build_task( + ctx: &a2a_server::ExecutorContext, + state: TaskState, + text: impl Into, +) -> Task { + let mut task = Task { + id: ctx.task_id.clone(), + context_id: ctx.context_id.clone(), + status: TaskStatus { + state, + message: None, + timestamp: None, + }, + artifacts: None, + history: ctx.message.clone().map(|message| vec![message]), + metadata: None, + }; + task.status.message = Some(build_task_message(&task.id, &task.context_id, text)); + task +} + +fn build_status_update( + ctx: &a2a_server::ExecutorContext, + state: TaskState, + text: impl Into, +) -> TaskStatusUpdateEvent { + TaskStatusUpdateEvent { + task_id: ctx.task_id.clone(), + context_id: ctx.context_id.clone(), + status: TaskStatus { + state, + message: Some(build_task_message(&ctx.task_id, &ctx.context_id, text)), + timestamp: None, + }, + metadata: None, + } +} + impl a2a_server::AgentExecutor for InteropExecutor { fn execute( &self, ctx: a2a_server::ExecutorContext, ) -> BoxStream<'static, Result> { - let response = StreamResponse::Message(build_response_message(ctx.message.as_ref())); + let response_text = format!("rust server received: {}", first_text(ctx.message.as_ref())); + let state = if first_text(ctx.message.as_ref()) == PENDING_REQUEST_TEXT { + TaskState::Working + } else { + TaskState::Completed + }; + let response = StreamResponse::Task(build_task(&ctx, state, response_text)); Box::pin(stream::once(async move { Ok(response) })) } fn cancel( &self, - _ctx: a2a_server::ExecutorContext, + ctx: a2a_server::ExecutorContext, ) -> BoxStream<'static, Result> { - Box::pin(stream::empty()) + let response = StreamResponse::StatusUpdate(build_status_update( + &ctx, + TaskState::Canceled, + "rust server canceled task", + )); + Box::pin(stream::once(async move { Ok(response) })) } } -fn parse_port() -> u16 { +fn parse_args() -> (u16, TransportProtocol, Option) { let mut args = env::args().skip(1); + let mut port = 19092; + let mut protocol = TransportProtocol::JsonRpc; + let mut grpc_port = None; while let Some(arg) = args.next() { - if arg == "--port" { - let value = args.next().expect("--port requires a numeric argument"); - return value.parse::().expect("--port value must fit in u16"); + match arg.as_str() { + "--port" => { + let value = args.next().expect("--port requires a numeric argument"); + port = value.parse::().expect("--port value must fit in u16"); + } + "--grpc-port" => { + let value = args + .next() + .expect("--grpc-port requires a numeric argument"); + grpc_port = Some( + value + .parse::() + .expect("--grpc-port value must fit in u16"), + ); + } + "--protocol" => { + let value = args + .next() + .expect("--protocol requires either 'jsonrpc', 'rest', or 'grpc'"); + protocol = TransportProtocol::from_str(&value); + } + other => panic!("unknown argument: {other}"), } } - 19092 + (port, protocol, grpc_port) } #[tokio::main] async fn main() { - let port = parse_port(); + let (port, protocol, grpc_port) = parse_args(); let handler = Arc::new(DefaultRequestHandler::new( InteropExecutor, InMemoryTaskStore::new(), )); - let card_producer = Arc::new(StaticAgentCard::new(build_agent_card(port))); + let card_producer = Arc::new(StaticAgentCard::new(build_agent_card( + port, protocol, grpc_port, + ))); + + let app = match protocol { + TransportProtocol::JsonRpc => Router::new() + .nest("/rpc", a2a_server::jsonrpc::jsonrpc_router(handler)) + .merge(a2a_server::agent_card::agent_card_router(card_producer)), + TransportProtocol::Rest => Router::new() + .merge(a2a_server::rest::rest_router(handler)) + .merge(a2a_server::agent_card::agent_card_router(card_producer)), + TransportProtocol::Grpc => { + let grpc_port = grpc_port.expect("gRPC transport requires --grpc-port"); + let card_listener = TcpListener::bind(("127.0.0.1", port)) + .await + .expect("agent card listener should bind"); + let card_app = + Router::new().merge(a2a_server::agent_card::agent_card_router(card_producer)); + let grpc_addr = format!("127.0.0.1:{grpc_port}") + .parse() + .expect("gRPC address should parse"); + let grpc_service = A2aServiceServer::new(GrpcHandler::new(handler)); + + println!( + "rust grpc fixture listening on http://127.0.0.1:{grpc_port} with card http://127.0.0.1:{port}" + ); - let app = Router::new() - .nest("/rpc", a2a_server::jsonrpc::jsonrpc_router(handler)) - .merge(a2a_server::agent_card::agent_card_router(card_producer)); + let card_server = tokio::spawn(async move { + axum::serve(card_listener, card_app) + .await + .expect("agent card server should run"); + }); + + Server::builder() + .add_service(grpc_service) + .serve(grpc_addr) + .await + .expect("gRPC server should run"); + + let _ = card_server.await; + + return; + } + }; let listener = TcpListener::bind(("127.0.0.1", port)) .await .expect("listener should bind"); - println!("rust jsonrpc fixture listening on http://127.0.0.1:{port}"); + println!( + "rust {} fixture listening on http://127.0.0.1:{port}", + protocol.name() + ); axum::serve(listener, app).await.expect("server should run"); } diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go index 334628a0..b995a523 100644 --- a/integrations/agntcy-a2a/tests/interop_rust_go_test.go +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -20,18 +20,43 @@ import ( "github.com/a2aproject/a2a-go/v2/a2a" "github.com/a2aproject/a2a-go/v2/a2aclient" "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard" + a2agrpc "github.com/a2aproject/a2a-go/v2/a2agrpc/v1" ginkgo "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) const ( - fixtureReadyTimeout = 20 * time.Second - probeTimeout = 2 * time.Minute - buildTimeout = 3 * time.Minute - stopTimeout = 5 * time.Second - requestText = "ping" + fixtureReadyTimeout = 20 * time.Second + probeTimeout = 2 * time.Minute + buildTimeout = 3 * time.Minute + stopTimeout = 5 * time.Second + requestText = "ping" + pendingRequestText = "pending" + requestDataKind = "structured" + requestDataScope = "interop" + requestMetadataKey = "csit" + requestMetadataValue = "multipart" + pushUnsupportedCode = -32003 + unsupportedOpCode = -32004 ) +type transportProtocol string + +const ( + transportJSONRPC transportProtocol = "jsonrpc" + transportREST transportProtocol = "rest" + transportGRPC transportProtocol = "grpc" +) + +type rustProbeOptions struct { + expectSubscribeUnsupported bool + expectPushUnsupported bool + relaxedErrorChecks bool + expectedPushErrorCode int +} + type fixtureBinaries struct { tempDir string goServer string @@ -143,6 +168,22 @@ func waitForReady(url string, done <-chan error, logs *lockedBuffer) error { return fmt.Errorf("timed out waiting for fixture readiness at %s\n%s", url, logs.String()) } +func waitForTCPListener(address string, logs *lockedBuffer) error { + deadline := time.Now().Add(fixtureReadyTimeout) + + for time.Now().Before(deadline) { + connection, err := net.DialTimeout("tcp", address, 500*time.Millisecond) + if err == nil { + connection.Close() + return nil + } + + time.Sleep(200 * time.Millisecond) + } + + return fmt.Errorf("timed out waiting for fixture listener at %s\n%s", address, logs.String()) +} + func executableName(name string) string { if runtime.GOOS == "windows" { return name + ".exe" @@ -222,29 +263,71 @@ func startFixtureProcess(name string, dir string, readyURL string, command strin return &fixtureProcess{name: name, cmd: cmd, cancel: cancel, done: done, logs: logs}, nil } -func startGoFixture(binaries fixtureBinaries, port int) (*fixtureProcess, string, error) { +func startGoFixture(binaries fixtureBinaries, port int, protocol transportProtocol) (*fixtureProcess, string, error) { baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) + args := []string{ + "--port", + fmt.Sprintf("%d", port), + "--protocol", + string(protocol), + } + grpcAddress := "" + if protocol == transportGRPC { + grpcPort := findFreePort() + grpcAddress = fmt.Sprintf("127.0.0.1:%d", grpcPort) + args = append(args, "--grpc-port", fmt.Sprintf("%d", grpcPort)) + } + process, err := startFixtureProcess( - "go-jsonrpc-server", + fmt.Sprintf("go-%s-server", protocol), componentRoot(), baseURL+"/.well-known/agent-card.json", binaries.goServer, - "--port", - fmt.Sprintf("%d", port), + args..., ) + if err != nil { + return nil, "", err + } + if grpcAddress != "" { + if err := waitForTCPListener(grpcAddress, process.logs); err != nil { + _ = process.stop() + return nil, "", fmt.Errorf("wait for go gRPC fixture listener: %w", err) + } + } return process, baseURL, err } -func startRustFixture(binaries fixtureBinaries, port int) (*fixtureProcess, string, error) { +func startRustFixture(binaries fixtureBinaries, port int, protocol transportProtocol) (*fixtureProcess, string, error) { baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) + args := []string{ + "--port", + fmt.Sprintf("%d", port), + "--protocol", + string(protocol), + } + grpcAddress := "" + if protocol == transportGRPC { + grpcPort := findFreePort() + grpcAddress = fmt.Sprintf("127.0.0.1:%d", grpcPort) + args = append(args, "--grpc-port", fmt.Sprintf("%d", grpcPort)) + } + process, err := startFixtureProcess( - "interop-rust-server", + fmt.Sprintf("rust-%s-server", protocol), componentRoot(), baseURL+"/.well-known/agent-card.json", binaries.rustServer, - "--port", - fmt.Sprintf("%d", port), + args..., ) + if err != nil { + return nil, "", err + } + if grpcAddress != "" { + if err := waitForTCPListener(grpcAddress, process.logs); err != nil { + _ = process.stop() + return nil, "", fmt.Errorf("wait for rust gRPC fixture listener: %w", err) + } + } return process, baseURL, err } @@ -254,7 +337,52 @@ func newGoClient(ctx context.Context, baseURL string) (*a2aclient.Client, error) return nil, err } - return a2aclient.NewFromCard(ctx, card) + return a2aclient.NewFromCard( + ctx, + card, + a2agrpc.WithGRPCTransport(grpc.WithTransportCredentials(insecure.NewCredentials())), + ) +} + +func newInteropRequest(text string, returnImmediately bool) *a2a.SendMessageRequest { + message := a2a.NewMessage( + a2a.MessageRoleUser, + a2a.NewTextPart(text), + a2a.NewDataPart(map[string]any{ + "kind": requestDataKind, + "scope": requestDataScope, + }), + ) + message.Metadata = map[string]any{ + requestMetadataKey: requestMetadataValue, + } + + return &a2a.SendMessageRequest{ + Message: message, + Config: &a2a.SendMessageConfig{ + ReturnImmediately: returnImmediately, + }, + } +} + +func assertMessageInteropPayload(message *a2a.Message, expectedText string, kind string) { + gomega.Expect(message).NotTo(gomega.BeNil(), kind) + + text, err := firstMessageText(message) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), kind) + gomega.Expect(text).To(gomega.Equal(expectedText), kind) + gomega.Expect(message.Parts).To(gomega.HaveLen(2), kind) + + dataPart, ok := message.Parts[1].Data().(map[string]any) + gomega.Expect(ok).To(gomega.BeTrue(), kind) + gomega.Expect(dataPart).To(gomega.HaveKeyWithValue("kind", requestDataKind), kind) + gomega.Expect(dataPart).To(gomega.HaveKeyWithValue("scope", requestDataScope), kind) + gomega.Expect(message.Metadata).To(gomega.HaveKeyWithValue(requestMetadataKey, requestMetadataValue), kind) +} + +func assertTaskHistoryPayload(task *a2a.Task, expectedText string, kind string) { + gomega.Expect(task.History).To(gomega.HaveLen(1), kind) + assertMessageInteropPayload(task.History[0], expectedText, kind) } func firstMessageText(message *a2a.Message) (string, error) { @@ -267,51 +395,171 @@ func firstMessageText(message *a2a.Message) (string, error) { return "", errors.New("message did not include a text part") } -func goClientUnaryText(ctx context.Context, client *a2aclient.Client) (string, error) { - result, err := client.SendMessage(ctx, &a2a.SendMessageRequest{ - Message: a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart(requestText)), - }) +func expectedServerText(serverPrefix string, text string) string { + return fmt.Sprintf("%s server received: %s", serverPrefix, text) +} + +func expectedCancelText(serverPrefix string) string { + return fmt.Sprintf("%s server canceled task", serverPrefix) +} + +func taskStatusText(task *a2a.Task) (string, error) { + if task == nil || task.Status.Message == nil { + return "", errors.New("task status did not include a message") + } + + return firstMessageText(task.Status.Message) +} + +func eventText(event a2a.Event) (string, bool, error) { + switch value := event.(type) { + case *a2a.Message: + text, err := firstMessageText(value) + return text, true, err + case *a2a.Task: + text, err := taskStatusText(value) + return text, true, err + case *a2a.TaskStatusUpdateEvent: + if value.Status.Message == nil { + return "", false, nil + } + text, err := firstMessageText(value.Status.Message) + return text, true, err + default: + return "", false, nil + } +} + +func goClientSendTask(ctx context.Context, client *a2aclient.Client, text string, returnImmediately bool) (*a2a.Task, error) { + result, err := client.SendMessage(ctx, newInteropRequest(text, returnImmediately)) if err != nil { - return "", err + return nil, err } - message, ok := result.(*a2a.Message) + task, ok := result.(*a2a.Task) if !ok { - return "", fmt.Errorf("unexpected unary response type %T", result) + return nil, fmt.Errorf("unexpected unary response type %T", result) } - return firstMessageText(message) + return task, nil } -func goClientStreamingText(ctx context.Context, client *a2aclient.Client) (string, error) { - request := &a2a.SendMessageRequest{ - Message: a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart(requestText)), +func goClientUnaryText(ctx context.Context, client *a2aclient.Client) (string, error) { + task, err := goClientSendTask(ctx, client, requestText, false) + if err != nil { + return "", err } + return taskStatusText(task) +} + +func goClientStreamingText(ctx context.Context, client *a2aclient.Client) (string, error) { + request := newInteropRequest(requestText, false) + for event, err := range client.SendStreamingMessage(ctx, request) { if err != nil { return "", err } - message, ok := event.(*a2a.Message) - if !ok { + text, hasText, textErr := eventText(event) + if textErr != nil { + return "", textErr + } + if !hasText { continue } - return firstMessageText(message) + return text, nil } return "", errors.New("stream completed without a message event") } -func runRustProbe(ctx context.Context, binaries fixtureBinaries, baseURL string, expectedText string) (string, error) { +func goClientAssertLifecycle(ctx context.Context, client *a2aclient.Client, serverPrefix string) { + completedTask, err := goClientSendTask(ctx, client, requestText, false) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(completedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) + + completedText, err := taskStatusText(completedTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(completedText).To(gomega.Equal(expectedServerText(serverPrefix, requestText))) + assertTaskHistoryPayload(completedTask, requestText, "completed task history") + + fetchedTask, err := client.GetTask(ctx, &a2a.GetTaskRequest{ID: completedTask.ID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(fetchedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) + + fetchedText, err := taskStatusText(fetchedTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(fetchedText).To(gomega.Equal(expectedServerText(serverPrefix, requestText))) + assertTaskHistoryPayload(fetchedTask, requestText, "fetched task history") + + listedTasks, err := client.ListTasks(ctx, &a2a.ListTasksRequest{ContextID: completedTask.ContextID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(listedTasks.Tasks).NotTo(gomega.BeEmpty()) + gomega.Expect(listedTasks.Tasks).To(gomega.ContainElement(gomega.HaveField("ID", completedTask.ID))) + + pendingTask, err := goClientSendTask(ctx, client, pendingRequestText, true) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(pendingTask.Status.State).To(gomega.Equal(a2a.TaskStateWorking)) + + pendingText, err := taskStatusText(pendingTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(pendingText).To(gomega.Equal(expectedServerText(serverPrefix, pendingRequestText))) + + canceledTask, err := client.CancelTask(ctx, &a2a.CancelTaskRequest{ID: pendingTask.ID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(canceledTask.Status.State).To(gomega.Equal(a2a.TaskStateCanceled)) + + canceledText, err := taskStatusText(canceledTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(canceledText).To(gomega.Equal(expectedCancelText(serverPrefix))) + + fetchedCanceledTask, err := client.GetTask(ctx, &a2a.GetTaskRequest{ID: pendingTask.ID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(fetchedCanceledTask.Status.State).To(gomega.Equal(a2a.TaskStateCanceled)) + + fetchedCanceledText, err := taskStatusText(fetchedCanceledTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(fetchedCanceledText).To(gomega.Equal(expectedCancelText(serverPrefix))) + + _, err = client.GetTask(ctx, &a2a.GetTaskRequest{ID: a2a.NewTaskID()}) + gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("task not found"))) + + _, err = client.CancelTask(ctx, &a2a.CancelTaskRequest{ID: completedTask.ID}) + gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("cancel"))) +} + +func runRustProbe( + ctx context.Context, + binaries fixtureBinaries, + baseURL string, + serverPrefix string, + options rustProbeOptions, +) (string, error) { + args := []string{ + "--card-url", + baseURL, + "--server-prefix", + serverPrefix, + } + if options.expectSubscribeUnsupported { + args = append(args, "--expect-subscribe-unsupported") + } + if options.expectPushUnsupported { + args = append(args, "--expect-push-unsupported") + if options.expectedPushErrorCode != 0 { + args = append(args, "--expected-push-error-code", fmt.Sprintf("%d", options.expectedPushErrorCode)) + } + } + if options.relaxedErrorChecks { + args = append(args, "--relaxed-error-checks") + } + cmd := exec.CommandContext( ctx, binaries.rustProbe, - "--card-url", - baseURL, - "--expect-text", - expectedText, + args..., ) cmd.Dir = componentRoot() @@ -325,11 +573,19 @@ func runRustProbe(ctx context.Context, binaries fixtureBinaries, baseURL string, var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func() { var ( - binaries fixtureBinaries - goFixture *fixtureProcess - rustFixture *fixtureProcess - goFixtureURL string - rustFixtureURL string + binaries fixtureBinaries + goJSONRPCFixture *fixtureProcess + rustJSONRPCFixture *fixtureProcess + goRESTFixture *fixtureProcess + rustRESTFixture *fixtureProcess + goGRPCFixture *fixtureProcess + rustGRPCFixture *fixtureProcess + goJSONRPCFixtureURL string + rustJSONRPCFixtureURL string + goRESTFixtureURL string + rustRESTFixtureURL string + goGRPCFixtureURL string + rustGRPCFixtureURL string ) ginkgo.BeforeAll(func() { @@ -338,70 +594,221 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func binaries, err = buildFixtureBinaries() gomega.Expect(err).NotTo(gomega.HaveOccurred()) - goFixture, goFixtureURL, err = startGoFixture(binaries, findFreePort()) + goJSONRPCFixture, goJSONRPCFixtureURL, err = startGoFixture(binaries, findFreePort(), transportJSONRPC) gomega.Expect(err).NotTo(gomega.HaveOccurred()) - rustFixture, rustFixtureURL, err = startRustFixture(binaries, findFreePort()) + rustJSONRPCFixture, rustJSONRPCFixtureURL, err = startRustFixture(binaries, findFreePort(), transportJSONRPC) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + goRESTFixture, goRESTFixtureURL, err = startGoFixture(binaries, findFreePort(), transportREST) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + rustRESTFixture, rustRESTFixtureURL, err = startRustFixture(binaries, findFreePort(), transportREST) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + goGRPCFixture, goGRPCFixtureURL, err = startGoFixture(binaries, findFreePort(), transportGRPC) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + rustGRPCFixture, rustGRPCFixtureURL, err = startRustFixture(binaries, findFreePort(), transportGRPC) gomega.Expect(err).NotTo(gomega.HaveOccurred()) }) ginkgo.AfterAll(func() { - if rustFixture != nil { - gomega.Expect(rustFixture.stop()).To(gomega.Succeed(), rustFixture.logs.String()) + if rustGRPCFixture != nil { + gomega.Expect(rustGRPCFixture.stop()).To(gomega.Succeed(), rustGRPCFixture.logs.String()) + } + if goGRPCFixture != nil { + gomega.Expect(goGRPCFixture.stop()).To(gomega.Succeed(), goGRPCFixture.logs.String()) + } + if rustRESTFixture != nil { + gomega.Expect(rustRESTFixture.stop()).To(gomega.Succeed(), rustRESTFixture.logs.String()) + } + if goRESTFixture != nil { + gomega.Expect(goRESTFixture.stop()).To(gomega.Succeed(), goRESTFixture.logs.String()) } - if goFixture != nil { - gomega.Expect(goFixture.stop()).To(gomega.Succeed(), goFixture.logs.String()) + if rustJSONRPCFixture != nil { + gomega.Expect(rustJSONRPCFixture.stop()).To(gomega.Succeed(), rustJSONRPCFixture.logs.String()) + } + if goJSONRPCFixture != nil { + gomega.Expect(goJSONRPCFixture.stop()).To(gomega.Succeed(), goJSONRPCFixture.logs.String()) } if binaries.tempDir != "" { gomega.Expect(os.RemoveAll(binaries.tempDir)).To(gomega.Succeed()) } }) - ginkgo.It("lets the Go client call the Go fixture", ginkgo.Label("go-go"), func(ctx ginkgo.SpecContext) { - requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) - defer cancel() + ginkgo.Context("JSON-RPC transport", func() { + ginkgo.It("lets the Go client call the Go fixture", ginkgo.Label("jsonrpc", "go-go"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() - client, err := newGoClient(requestCtx, goFixtureURL) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) + client, err := newGoClient(requestCtx, goJSONRPCFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) - unaryText, err := goClientUnaryText(requestCtx, client) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(unaryText).To(gomega.Equal("go server received: ping")) + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal(expectedServerText("go", requestText))) - streamText, err := goClientStreamingText(requestCtx, client) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(streamText).To(gomega.Equal("go server received: ping")) - }) + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal(expectedServerText("go", requestText))) - ginkgo.It("lets the Go client call the Rust fixture", ginkgo.Label("go-rust"), func(ctx ginkgo.SpecContext) { - requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) - defer cancel() + goClientAssertLifecycle(requestCtx, client, "go") + }) - client, err := newGoClient(requestCtx, rustFixtureURL) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) + ginkgo.It("lets the Go client call the Rust fixture", ginkgo.Label("jsonrpc", "go-rust"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() - unaryText, err := goClientUnaryText(requestCtx, client) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(unaryText).To(gomega.Equal("rust server received: ping")) + client, err := newGoClient(requestCtx, rustJSONRPCFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) - streamText, err := goClientStreamingText(requestCtx, client) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(streamText).To(gomega.Equal("rust server received: ping")) - }) + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal(expectedServerText("rust", requestText))) - ginkgo.It("lets the Rust client call the Go fixture", ginkgo.Label("rust-go"), func(ctx ginkgo.SpecContext) { - requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) - defer cancel() + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) - output, err := runRustProbe(requestCtx, binaries, goFixtureURL, "go server received: ping") - gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + goClientAssertLifecycle(requestCtx, client, "rust") + }) + + ginkgo.It("lets the Rust client call the Go fixture", ginkgo.Label("jsonrpc", "rust-go"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, goJSONRPCFixtureURL, "go", rustProbeOptions{ + expectPushUnsupported: true, + expectedPushErrorCode: pushUnsupportedCode, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) + + ginkgo.It("lets the Rust client call the Rust fixture", ginkgo.Label("jsonrpc", "rust-rust"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, rustJSONRPCFixtureURL, "rust", rustProbeOptions{ + expectPushUnsupported: true, + expectedPushErrorCode: pushUnsupportedCode, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) }) - ginkgo.It("lets the Rust client call the Rust fixture", ginkgo.Label("rust-rust"), func(ctx ginkgo.SpecContext) { - requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) - defer cancel() + ginkgo.Context("HTTP+JSON transport", func() { + ginkgo.It("lets the Go client call the Go fixture over REST", ginkgo.Label("rest", "go-go"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + client, err := newGoClient(requestCtx, goRESTFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal(expectedServerText("go", requestText))) + + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal(expectedServerText("go", requestText))) + + goClientAssertLifecycle(requestCtx, client, "go") + }) + + ginkgo.It("lets the Go client call the Rust fixture over REST", ginkgo.Label("rest", "go-rust"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + client, err := newGoClient(requestCtx, rustRESTFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal(expectedServerText("rust", requestText))) + + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) + + goClientAssertLifecycle(requestCtx, client, "rust") + }) + + ginkgo.It("lets the Rust client call the Go fixture over REST", ginkgo.Label("rest", "rust-go"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, goRESTFixtureURL, "go", rustProbeOptions{ + expectPushUnsupported: true, + expectedPushErrorCode: pushUnsupportedCode, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) + + ginkgo.It("lets the Rust client call the Rust fixture over REST", ginkgo.Label("rest", "rust-rust"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, rustRESTFixtureURL, "rust", rustProbeOptions{ + expectPushUnsupported: true, + expectedPushErrorCode: pushUnsupportedCode, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) + }) - output, err := runRustProbe(requestCtx, binaries, rustFixtureURL, "rust server received: ping") - gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + ginkgo.Context("gRPC transport", func() { + ginkgo.It("lets the Go client call the Go fixture over gRPC", ginkgo.Label("grpc", "go-go"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + client, err := newGoClient(requestCtx, goGRPCFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal(expectedServerText("go", requestText))) + + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal(expectedServerText("go", requestText))) + + goClientAssertLifecycle(requestCtx, client, "go") + }) + + ginkgo.It("documents the Go client gRPC endpoint mismatch against the Rust fixture", ginkgo.Label("grpc", "go-rust"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + client, err := newGoClient(requestCtx, rustGRPCFixtureURL) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + _, err = goClientUnaryText(requestCtx, client) + gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("too many colons in address"))) + }) + + ginkgo.It("documents the Rust client gRPC endpoint mismatch against the Go fixture", ginkgo.Label("grpc", "rust-go"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, goGRPCFixtureURL, "go", rustProbeOptions{ + expectPushUnsupported: true, + expectedPushErrorCode: unsupportedOpCode, + }) + gomega.Expect(err).To(gomega.HaveOccurred()) + gomega.Expect(output).To(gomega.ContainSubstring("client creation failed")) + gomega.Expect(output).To(gomega.ContainSubstring("gRPC connect error")) + }) + + ginkgo.It("lets the Rust client call the Rust fixture over gRPC", ginkgo.Label("grpc", "rust-rust"), func(ctx ginkgo.SpecContext) { + requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) + defer cancel() + + output, err := runRustProbe(requestCtx, binaries, rustGRPCFixtureURL, "rust", rustProbeOptions{ + expectPushUnsupported: true, + expectedPushErrorCode: unsupportedOpCode, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) + }) }) }) diff --git a/integrations/go.mod b/integrations/go.mod index 897e4f84..0a927384 100644 --- a/integrations/go.mod +++ b/integrations/go.mod @@ -7,6 +7,7 @@ require ( github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.0 github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.79.3 k8s.io/api v0.33.0 k8s.io/apimachinery v0.33.0 k8s.io/client-go v0.33.0 @@ -16,13 +17,15 @@ require ( github.com/google/go-cmp v0.7.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/sync v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -42,7 +45,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/integrations/go.sum b/integrations/go.sum index 7e3d6b43..3c23bef3 100644 --- a/integrations/go.sum +++ b/integrations/go.sum @@ -1,5 +1,7 @@ github.com/a2aproject/a2a-go/v2 v2.1.0 h1:mtn3UR+B8RnIRYTVNUHmip8FMCK5Pe3Id2aNATOWEPw= github.com/a2aproject/a2a-go/v2 v2.1.0/go.mod h1:nm/NLcGWEQsVf7rgcLy74DyswS5BanPS6ubQhGhiQaA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,8 +11,10 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= @@ -23,6 +27,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -83,6 +89,18 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -96,8 +114,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -126,6 +144,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 5145cc5b98da9f7048175be828ed2ba6ece0c0d8 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sun, 5 Apr 2026 23:27:09 +0200 Subject: [PATCH 09/15] test: update CSIT for released Rust crates Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/README.md | 8 +- .../agntcy-a2a/fixtures/rust/Cargo.lock | 155 ++++++++++++++++-- .../agntcy-a2a/fixtures/rust/Cargo.toml | 8 +- .../agntcy-a2a/tests/interop_rust_go_test.go | 6 +- 4 files changed, 149 insertions(+), 28 deletions(-) diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index 43273675..dafdb307 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -6,12 +6,12 @@ The current slice covers Rust and Go across the released JSON-RPC, HTTP+JSON, an JSON-RPC and HTTP+JSON are green across the full 4-leg Rust/Go matrix. -gRPC currently has two passing same-SDK legs plus two explicit incompatibility checks: +gRPC now has three passing legs plus one remaining documented incompatibility: - Go client -> Go server passes with the Go SDK's native bare `host:port` gRPC endpoint form +- Rust client -> Go server passes because the released Rust gRPC client now accepts bare endpoints advertised in the Go card - Rust client -> Rust server passes with the Rust SDK's native `http://host:port` gRPC endpoint form -- Go client -> Rust server documents that the released Go gRPC client rejects scheme-prefixed endpoints advertised in the Rust card -- Rust client -> Go server documents that the released Rust gRPC client rejects the bare endpoint advertised in the Go card +- Go client -> Rust server still documents that the released Go gRPC client rejects scheme-prefixed endpoints advertised in the Rust card Each leg validates the same reusable behavior: @@ -41,7 +41,7 @@ The gRPC legs follow the same agent-card discovery path as the other transports: | HTTP+JSON | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:rest:rust-rust` | `task integrations:a2a:test:rust-go:rest:rust-rust` | | gRPC | `go-go` | Go client -> Go server | Pass | `task test:rust-go:grpc:go-go` | `task integrations:a2a:test:rust-go:grpc:go-go` | | gRPC | `go-rust` | Go client -> Rust server | Incompatibility documented | `task test:rust-go:grpc:go-rust` | `task integrations:a2a:test:rust-go:grpc:go-rust` | -| gRPC | `rust-go` | Rust client -> Go server | Incompatibility documented | `task test:rust-go:grpc:rust-go` | `task integrations:a2a:test:rust-go:grpc:rust-go` | +| gRPC | `rust-go` | Rust client -> Go server | Pass | `task test:rust-go:grpc:rust-go` | `task integrations:a2a:test:rust-go:grpc:rust-go` | | gRPC | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:grpc:rust-rust` | `task integrations:a2a:test:rust-go:grpc:rust-rust` | ## Running the Suite diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index 19b55546..704fd2b5 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -8,7 +8,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "042c4a30365eb47860f3202259b174f1ddefe5206331f364d6072daf106d9d38" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "serde", "serde_json", @@ -18,11 +18,12 @@ dependencies = [ [[package]] name = "agntcy-a2a-client" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c311e723bc040d93f1382a845caf4ee5504c6049d83baf8ec42ff36001e737b" +checksum = "4874ed7c7560ff9a05060959faf9ad6a3cf72dd4e4dd16fa8b73c6508092820c" dependencies = [ "agntcy-a2a", + "agntcy-a2a-pb", "async-trait", "bytes", "futures", @@ -55,9 +56,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-grpc" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b470f3792b40c58ce0d957c2263a397f7a34508afa84755ff5357745228f8afe" +checksum = "4e5be1c28f0fdb5077d62c6989d10add6700e4e1f9d9802bef614ee21c0a2b59" dependencies = [ "agntcy-a2a", "agntcy-a2a-client", @@ -74,14 +75,19 @@ dependencies = [ [[package]] name = "agntcy-a2a-pb" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091a02c81d4f9e136ed454860dd8a869cb47cf685a37b8ae5175216e29b87cb6" +checksum = "a3b9d24ed1284ea39632a36b901c610833190aabf5061dd8dc2d084e299e1ad8" dependencies = [ "agntcy-a2a", "chrono", + "pbjson", + "pbjson-build", + "pbjson-types", "prost", "prost-types", + "protoc-bin-vendored", + "serde", "serde_json", "tonic", "tonic-build", @@ -89,11 +95,12 @@ dependencies = [ [[package]] name = "agntcy-a2a-server" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1564f813121e70fc563b8fb013c3e2be5bdffd37049384bcbfbd348276e834c" +checksum = "5fbaaad421df1b96f3ff368518f040d29943edbaddc01d0312de72cd08c1fa7e" dependencies = [ "agntcy-a2a", + "agntcy-a2a-pb", "async-trait", "axum", "chrono", @@ -221,6 +228,12 @@ dependencies = [ "syn", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -345,9 +358,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" [[package]] name = "find-msvc-tools" @@ -667,7 +680,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -847,6 +860,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1047,6 +1069,43 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pbjson" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e6349fa080353f4a597daffd05cb81572a9c031a6d4fff7e504947496fcc68" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "pbjson-build" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eea3058763d6e656105d1403cb04e0a41b7bbac6362d413e7c33be0c32279c9" +dependencies = [ + "heck", + "itertools 0.13.0", + "prost", + "prost-types", +] + +[[package]] +name = "pbjson-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54e5e7bfb1652f95bc361d76f3c780d8e526b134b85417e774166ee941f0887" +dependencies = [ + "bytes", + "chrono", + "pbjson", + "pbjson-build", + "prost", + "prost-build", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1140,7 +1199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -1160,7 +1219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -1175,6 +1234,70 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + [[package]] name = "quote" version = "1.0.45" @@ -1234,7 +1357,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -1688,7 +1811,7 @@ checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "h2", "http", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index f395fb95..0b4d2169 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -6,10 +6,10 @@ publish = false [dependencies] a2a = { package = "agntcy-a2a", version = "0.2.3" } -a2a-client = { package = "agntcy-a2a-client", version = "0.1.7" } -a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.4" } -a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.3" } -a2a-server = { package = "agntcy-a2a-server", version = "0.2.0" } +a2a-client = { package = "agntcy-a2a-client", version = "0.1.8" } +a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.5" } +a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.4" } +a2a-server = { package = "agntcy-a2a-server", version = "0.2.1" } axum = "0.8" futures = "0.3" serde_json = "1" diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go index b995a523..491cb899 100644 --- a/integrations/agntcy-a2a/tests/interop_rust_go_test.go +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -787,7 +787,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("too many colons in address"))) }) - ginkgo.It("documents the Rust client gRPC endpoint mismatch against the Go fixture", ginkgo.Label("grpc", "rust-go"), func(ctx ginkgo.SpecContext) { + ginkgo.It("lets the Rust client call the Go fixture over gRPC", ginkgo.Label("grpc", "rust-go"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() @@ -795,9 +795,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func expectPushUnsupported: true, expectedPushErrorCode: unsupportedOpCode, }) - gomega.Expect(err).To(gomega.HaveOccurred()) - gomega.Expect(output).To(gomega.ContainSubstring("client creation failed")) - gomega.Expect(output).To(gomega.ContainSubstring("gRPC connect error")) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) }) ginkgo.It("lets the Rust client call the Rust fixture over gRPC", ginkgo.Label("grpc", "rust-rust"), func(ctx ginkgo.SpecContext) { From f0cb8a06bfc002fa344922a44dae3c12c05b8cd7 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Mon, 6 Apr 2026 00:35:33 +0200 Subject: [PATCH 10/15] chore: refresh released Rust fixture crates Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/README.md | 8 ++--- .../agntcy-a2a/fixtures/rust/Cargo.lock | 20 +++++------ .../agntcy-a2a/fixtures/rust/Cargo.toml | 10 +++--- .../agntcy-a2a/tests/interop_rust_go_test.go | 34 +++++++++++++++++-- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index dafdb307..75631db2 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -6,12 +6,12 @@ The current slice covers Rust and Go across the released JSON-RPC, HTTP+JSON, an JSON-RPC and HTTP+JSON are green across the full 4-leg Rust/Go matrix. -gRPC now has three passing legs plus one remaining documented incompatibility: +gRPC still has three passing legs plus one remaining documented incompatibility after the released Rust agent-card endpoint fix: - Go client -> Go server passes with the Go SDK's native bare `host:port` gRPC endpoint form -- Rust client -> Go server passes because the released Rust gRPC client now accepts bare endpoints advertised in the Go card -- Rust client -> Rust server passes with the Rust SDK's native `http://host:port` gRPC endpoint form -- Go client -> Rust server still documents that the released Go gRPC client rejects scheme-prefixed endpoints advertised in the Rust card +- Rust client -> Go server passes because the released Rust gRPC client accepts bare endpoints advertised in the Go card +- Rust client -> Rust server passes with the released Rust SDK's published gRPC endpoint form +- Go client -> Rust server now clears transport discovery plus unary and streaming exchange, but `ListTasks` still returns an empty task list against the Rust gRPC server Each leg validates the same reusable behavior: diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index 704fd2b5..4ff91392 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "agntcy-a2a" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "042c4a30365eb47860f3202259b174f1ddefe5206331f364d6072daf106d9d38" +checksum = "1be0b586d63f3f6ca6c19f6d45c174413371bab9a68a25bf8167938791779f0e" dependencies = [ "base64 0.22.1", "chrono", @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-client" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4874ed7c7560ff9a05060959faf9ad6a3cf72dd4e4dd16fa8b73c6508092820c" +checksum = "835adcc768160d90a000002fa0d21e446b0954f3975b7f0f4692804ab50a6220" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-grpc" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5be1c28f0fdb5077d62c6989d10add6700e4e1f9d9802bef614ee21c0a2b59" +checksum = "13607a0a45247a9d37b24100b228fd190bd3b28a54f6cf3dedd949b3053a4c8c" dependencies = [ "agntcy-a2a", "agntcy-a2a-client", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-pb" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b9d24ed1284ea39632a36b901c610833190aabf5061dd8dc2d084e299e1ad8" +checksum = "c1711a80877ac77ac5b784d05e3aba18a1a98bf86949ceb2991fd723d15cec6b" dependencies = [ "agntcy-a2a", "chrono", @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-server" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbaaad421df1b96f3ff368518f040d29943edbaddc01d0312de72cd08c1fa7e" +checksum = "059edc51da36774c3ea627e63037b77ab69295c70a93d17e4b9498dc748ec758" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index 0b4d2169..ce976031 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -5,11 +5,11 @@ edition = "2024" publish = false [dependencies] -a2a = { package = "agntcy-a2a", version = "0.2.3" } -a2a-client = { package = "agntcy-a2a-client", version = "0.1.8" } -a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.5" } -a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.4" } -a2a-server = { package = "agntcy-a2a-server", version = "0.2.1" } +a2a = { package = "agntcy-a2a", version = "0.2.4" } +a2a-client = { package = "agntcy-a2a-client", version = "0.1.9" } +a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.6" } +a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.5" } +a2a-server = { package = "agntcy-a2a-server", version = "0.2.2" } axum = "0.8" futures = "0.3" serde_json = "1" diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go index 491cb899..8bdabfbf 100644 --- a/integrations/agntcy-a2a/tests/interop_rust_go_test.go +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -776,15 +776,43 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func goClientAssertLifecycle(requestCtx, client, "go") }) - ginkgo.It("documents the Go client gRPC endpoint mismatch against the Rust fixture", ginkgo.Label("grpc", "go-rust"), func(ctx ginkgo.SpecContext) { + ginkgo.It("documents the remaining Go client list_tasks mismatch against the Rust fixture over gRPC", ginkgo.Label("grpc", "go-rust"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() client, err := newGoClient(requestCtx, rustGRPCFixtureURL) gomega.Expect(err).NotTo(gomega.HaveOccurred()) - _, err = goClientUnaryText(requestCtx, client) - gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("too many colons in address"))) + unaryText, err := goClientUnaryText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(unaryText).To(gomega.Equal(expectedServerText("rust", requestText))) + + streamText, err := goClientStreamingText(requestCtx, client) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) + + completedTask, err := goClientSendTask(requestCtx, client, requestText, false) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(completedTask.ContextID).NotTo(gomega.BeEmpty()) + gomega.Expect(completedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) + + completedText, err := taskStatusText(completedTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(completedText).To(gomega.Equal(expectedServerText("rust", requestText))) + assertTaskHistoryPayload(completedTask, requestText, "completed task history") + + fetchedTask, err := client.GetTask(requestCtx, &a2a.GetTaskRequest{ID: completedTask.ID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(fetchedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) + + fetchedText, err := taskStatusText(fetchedTask) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(fetchedText).To(gomega.Equal(expectedServerText("rust", requestText))) + assertTaskHistoryPayload(fetchedTask, requestText, "fetched task history") + + listedTasks, err := client.ListTasks(requestCtx, &a2a.ListTasksRequest{ContextID: completedTask.ContextID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(listedTasks.Tasks).To(gomega.BeEmpty()) }) ginkgo.It("lets the Rust client call the Go fixture over gRPC", ginkgo.Label("grpc", "rust-go"), func(ctx ginkgo.SpecContext) { From 308551ca5bfdad61f79c7ce6b951c1f0597d275a Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Mon, 6 Apr 2026 11:10:47 +0200 Subject: [PATCH 11/15] test(a2a): refresh released Rust interop coverage Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/README.md | 17 ++- .../agntcy-a2a/fixtures/rust/Cargo.lock | 16 +-- .../agntcy-a2a/fixtures/rust/Cargo.toml | 8 +- .../rust/src/bin/interop-rust-probe.rs | 123 ++++++++++++++++++ .../rust/src/bin/interop-rust-server.rs | 14 +- .../agntcy-a2a/tests/interop_rust_go_test.go | 105 ++++++++++----- 6 files changed, 221 insertions(+), 62 deletions(-) diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index 75631db2..e760a916 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -4,21 +4,20 @@ This component hosts cross-SDK A2A interoperability checks. The current slice covers Rust and Go across the released JSON-RPC, HTTP+JSON, and gRPC bindings. -JSON-RPC and HTTP+JSON are green across the full 4-leg Rust/Go matrix. +All 12 Rust/Go client-server legs in the current core lifecycle matrix are green across JSON-RPC, HTTP+JSON, and gRPC. -gRPC still has three passing legs plus one remaining documented incompatibility after the released Rust agent-card endpoint fix: +The released Rust fixture now exposes push-config CRUD. CSIT validates that path from the Rust client across all three transports and from the Go client on the gRPC `go-rust` leg. -- Go client -> Go server passes with the Go SDK's native bare `host:port` gRPC endpoint form -- Rust client -> Go server passes because the released Rust gRPC client accepts bare endpoints advertised in the Go card -- Rust client -> Rust server passes with the released Rust SDK's published gRPC endpoint form -- Go client -> Rust server now clears transport discovery plus unary and streaming exchange, but `ListTasks` still returns an empty task list against the Rust gRPC server +Go client -> Rust server push-config creation over JSON-RPC and HTTP+JSON still has a request-shape mismatch, so those two transports remain outside the promoted push-config coverage. -Each leg validates the same reusable behavior: +The Go fixture still models the current unsupported push-config behavior, and the Rust-probe scenarios keep validating that negative path against Go-server targets. + +Across the matrix, the scenarios validate the same core interoperability behavior: - unary and streaming `SendMessage` - lifecycle methods across `GetTask`, `ListTasks`, and `CancelTask` - negative-path error semantics for missing and non-cancelable tasks -- unsupported push-notification behavior +- successful push-config CRUD on the promoted Rust-server paths and unsupported push-config errors against the Go fixture where exercised - preservation of a mixed text plus structured-data request payload and message metadata through task history The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. @@ -40,7 +39,7 @@ The gRPC legs follow the same agent-card discovery path as the other transports: | HTTP+JSON | `rust-go` | Rust client -> Go server | Pass | `task test:rust-go:rest:rust-go` | `task integrations:a2a:test:rust-go:rest:rust-go` | | HTTP+JSON | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:rest:rust-rust` | `task integrations:a2a:test:rust-go:rest:rust-rust` | | gRPC | `go-go` | Go client -> Go server | Pass | `task test:rust-go:grpc:go-go` | `task integrations:a2a:test:rust-go:grpc:go-go` | -| gRPC | `go-rust` | Go client -> Rust server | Incompatibility documented | `task test:rust-go:grpc:go-rust` | `task integrations:a2a:test:rust-go:grpc:go-rust` | +| gRPC | `go-rust` | Go client -> Rust server | Pass | `task test:rust-go:grpc:go-rust` | `task integrations:a2a:test:rust-go:grpc:go-rust` | | gRPC | `rust-go` | Rust client -> Go server | Pass | `task test:rust-go:grpc:rust-go` | `task integrations:a2a:test:rust-go:grpc:rust-go` | | gRPC | `rust-rust` | Rust client -> Rust server | Pass | `task test:rust-go:grpc:rust-rust` | `task integrations:a2a:test:rust-go:grpc:rust-rust` | diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index 4ff91392..43704db1 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-client" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835adcc768160d90a000002fa0d21e446b0954f3975b7f0f4692804ab50a6220" +checksum = "1945dc35315d0d782f1ef3075cb2836370ad97e341c075f151d8954487588f67" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-grpc" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13607a0a45247a9d37b24100b228fd190bd3b28a54f6cf3dedd949b3053a4c8c" +checksum = "6d17f8b8655b905db5a1a8f2384df2f06815b62fa50a34fa7e8693d2f6a7c841" dependencies = [ "agntcy-a2a", "agntcy-a2a-client", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-pb" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1711a80877ac77ac5b784d05e3aba18a1a98bf86949ceb2991fd723d15cec6b" +checksum = "cff3daf98c4dbed9b204903dc4567133462e849f3469b907744d0a7780cbc8bd" dependencies = [ "agntcy-a2a", "chrono", @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-server" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059edc51da36774c3ea627e63037b77ab69295c70a93d17e4b9498dc748ec758" +checksum = "68da8233159c97b16f9bc9c7b7ea15aa43e04738e4e519613f4c0b29500431f0" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index ce976031..9abb9801 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -6,10 +6,10 @@ publish = false [dependencies] a2a = { package = "agntcy-a2a", version = "0.2.4" } -a2a-client = { package = "agntcy-a2a-client", version = "0.1.9" } -a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.6" } -a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.5" } -a2a-server = { package = "agntcy-a2a-server", version = "0.2.2" } +a2a-client = { package = "agntcy-a2a-client", version = "0.1.10" } +a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.7" } +a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.6" } +a2a-server = { package = "agntcy-a2a-server", version = "0.2.3" } axum = "0.8" futures = "0.3" serde_json = "1" diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs index 2276c0f4..23562bd9 100644 --- a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-probe.rs @@ -24,6 +24,7 @@ struct Args { card_url: String, server_prefix: String, expect_subscribe_unsupported: bool, + expect_push_supported: bool, expect_push_unsupported: bool, relaxed_error_checks: bool, expected_push_error_code: i32, @@ -34,6 +35,7 @@ fn parse_args() -> Result { let mut card_url = None; let mut server_prefix = None; let mut expect_subscribe_unsupported = false; + let mut expect_push_supported = false; let mut expect_push_unsupported = false; let mut relaxed_error_checks = false; let mut expected_push_error_code = a2a::error_code::PUSH_NOTIFICATION_NOT_SUPPORTED; @@ -55,6 +57,9 @@ fn parse_args() -> Result { "--expect-subscribe-unsupported" => { expect_subscribe_unsupported = true; } + "--expect-push-supported" => { + expect_push_supported = true; + } "--expect-push-unsupported" => { expect_push_unsupported = true; } @@ -75,10 +80,18 @@ fn parse_args() -> Result { } } + if expect_push_supported && expect_push_unsupported { + return Err( + "--expect-push-supported and --expect-push-unsupported are mutually exclusive" + .to_string(), + ); + } + Ok(Args { card_url: card_url.ok_or_else(|| "missing --card-url".to_string())?, server_prefix: server_prefix.ok_or_else(|| "missing --server-prefix".to_string())?, expect_subscribe_unsupported, + expect_push_supported, expect_push_unsupported, relaxed_error_checks, expected_push_error_code, @@ -138,6 +151,29 @@ fn assert_failed(result: Result, kind: &str) -> Result<(), Strin } } +fn assert_push_config( + actual: &TaskPushNotificationConfig, + task_id: &str, + expected: &PushNotificationConfig, + kind: &str, +) -> Result<(), String> { + if actual.task_id != task_id { + return Err(format!( + "unexpected {kind} task id: got {:?}, want {:?}", + actual.task_id, task_id + )); + } + + if actual.config != *expected { + return Err(format!( + "unexpected {kind} push config: got {:?}, want {:?}", + actual.config, expected + )); + } + + Ok(()) +} + async fn assert_stream_error_code( result: Result>, A2AError>, expected_code: i32, @@ -617,6 +653,93 @@ async fn run(args: Args) -> Result<(), String> { "delete_push_config", )?; } + } else if args.expect_push_supported { + let push_config = PushNotificationConfig { + url: "https://example.invalid/webhook".to_string(), + id: Some("interop-config".to_string()), + token: Some("interop-token".to_string()), + authentication: Some(AuthenticationInfo { + scheme: "Bearer".to_string(), + credentials: Some("interop-credential".to_string()), + }), + }; + + let created_push_config = client + .create_push_config(&CreateTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + config: push_config.clone(), + tenant: None, + }) + .await + .map_err(|error| format!("create_push_config failed: {error}"))?; + assert_push_config( + &created_push_config, + &completed_task.id, + &push_config, + "create_push_config", + )?; + + let fetched_push_config = client + .get_push_config(&GetTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + id: "interop-config".to_string(), + tenant: None, + }) + .await + .map_err(|error| format!("get_push_config failed: {error}"))?; + assert_push_config( + &fetched_push_config, + &completed_task.id, + &push_config, + "get_push_config", + )?; + + let listed_push_configs = client + .list_push_configs(&ListTaskPushNotificationConfigsRequest { + task_id: completed_task.id.clone(), + page_size: None, + page_token: None, + tenant: None, + }) + .await + .map_err(|error| format!("list_push_configs failed: {error}"))?; + if listed_push_configs.configs.len() != 1 { + return Err(format!( + "unexpected list_push_configs result count: got {}, want 1", + listed_push_configs.configs.len() + )); + } + assert_push_config( + &listed_push_configs.configs[0], + &completed_task.id, + &push_config, + "list_push_configs", + )?; + + client + .delete_push_config(&DeleteTaskPushNotificationConfigRequest { + task_id: completed_task.id.clone(), + id: "interop-config".to_string(), + tenant: None, + }) + .await + .map_err(|error| format!("delete_push_config failed: {error}"))?; + + let listed_after_delete = client + .list_push_configs(&ListTaskPushNotificationConfigsRequest { + task_id: completed_task.id.clone(), + page_size: None, + page_token: None, + tenant: None, + }) + .await + .map_err(|error| format!("list_push_configs after delete failed: {error}"))?; + if !listed_after_delete.configs.is_empty() { + return Err(format!( + "expected list_push_configs after delete to be empty, got {:?}", + listed_after_delete.configs + )); + } } let protocol = card diff --git a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs index a25473d9..47d9e822 100644 --- a/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs +++ b/integrations/agntcy-a2a/fixtures/rust/src/bin/interop-rust-server.rs @@ -7,7 +7,9 @@ use std::sync::Arc; use a2a::*; use a2a_grpc::GrpcHandler; use a2a_pb::proto::a2a_service_server::A2aServiceServer; -use a2a_server::{DefaultRequestHandler, InMemoryTaskStore, StaticAgentCard}; +use a2a_server::{ + DefaultRequestHandler, InMemoryPushConfigStore, InMemoryTaskStore, StaticAgentCard, +}; use axum::Router; use futures::stream::{self, BoxStream}; use tokio::net::TcpListener; @@ -84,7 +86,7 @@ fn build_agent_card( supported_interfaces: vec![AgentInterface::new(interface_url, binding)], capabilities: AgentCapabilities { streaming: Some(true), - push_notifications: Some(false), + push_notifications: Some(true), extensions: None, extended_agent_card: None, }, @@ -221,10 +223,10 @@ fn parse_args() -> (u16, TransportProtocol, Option) { #[tokio::main] async fn main() { let (port, protocol, grpc_port) = parse_args(); - let handler = Arc::new(DefaultRequestHandler::new( - InteropExecutor, - InMemoryTaskStore::new(), - )); + let handler = Arc::new( + DefaultRequestHandler::new(InteropExecutor, InMemoryTaskStore::new()) + .with_push_config_store(InMemoryPushConfigStore::new()), + ); let card_producer = Arc::new(StaticAgentCard::new(build_agent_card( port, protocol, grpc_port, ))); diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go index 8bdabfbf..462a57f1 100644 --- a/integrations/agntcy-a2a/tests/interop_rust_go_test.go +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -52,6 +52,7 @@ const ( type rustProbeOptions struct { expectSubscribeUnsupported bool + expectPushSupported bool expectPushUnsupported bool relaxedErrorChecks bool expectedPushErrorCode int @@ -403,6 +404,57 @@ func expectedCancelText(serverPrefix string) string { return fmt.Sprintf("%s server canceled task", serverPrefix) } +func newInteropPushConfig() *a2a.PushConfig { + return &a2a.PushConfig{ + ID: "interop-config", + URL: "https://example.invalid/webhook", + Token: "interop-token", + Auth: &a2a.PushAuthInfo{ + Scheme: "Bearer", + Credentials: "interop-credential", + }, + } +} + +func assertTaskPushConfig(config *a2a.TaskPushConfig, taskID a2a.TaskID, expected *a2a.PushConfig, kind string) { + gomega.Expect(config).NotTo(gomega.BeNil(), kind) + gomega.Expect(config.TaskID).To(gomega.Equal(taskID), kind) + gomega.Expect(config.Config).To(gomega.Equal(*expected), kind) +} + +func goClientAssertPushLifecycle(ctx context.Context, client *a2aclient.Client, taskID a2a.TaskID) { + pushConfig := newInteropPushConfig() + + createdConfig, err := client.CreateTaskPushConfig(ctx, &a2a.CreateTaskPushConfigRequest{ + TaskID: taskID, + Config: *pushConfig, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + assertTaskPushConfig(createdConfig, taskID, pushConfig, "created push config") + + fetchedConfig, err := client.GetTaskPushConfig(ctx, &a2a.GetTaskPushConfigRequest{ + TaskID: taskID, + ID: pushConfig.ID, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + assertTaskPushConfig(fetchedConfig, taskID, pushConfig, "fetched push config") + + listedConfigs, err := client.ListTaskPushConfigs(ctx, &a2a.ListTaskPushConfigRequest{TaskID: taskID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(listedConfigs).To(gomega.HaveLen(1)) + assertTaskPushConfig(listedConfigs[0], taskID, pushConfig, "listed push config") + + err = client.DeleteTaskPushConfig(ctx, &a2a.DeleteTaskPushConfigRequest{ + TaskID: taskID, + ID: pushConfig.ID, + }) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + listedConfigs, err = client.ListTaskPushConfigs(ctx, &a2a.ListTaskPushConfigRequest{TaskID: taskID}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(listedConfigs).To(gomega.BeEmpty()) +} + func taskStatusText(task *a2a.Task) (string, error) { if task == nil || task.Status.Message == nil { return "", errors.New("task status did not include a message") @@ -475,7 +527,7 @@ func goClientStreamingText(ctx context.Context, client *a2aclient.Client) (strin return "", errors.New("stream completed without a message event") } -func goClientAssertLifecycle(ctx context.Context, client *a2aclient.Client, serverPrefix string) { +func goClientAssertLifecycle(ctx context.Context, client *a2aclient.Client, serverPrefix string, expectPushSupported bool) { completedTask, err := goClientSendTask(ctx, client, requestText, false) gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(completedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) @@ -499,6 +551,10 @@ func goClientAssertLifecycle(ctx context.Context, client *a2aclient.Client, serv gomega.Expect(listedTasks.Tasks).NotTo(gomega.BeEmpty()) gomega.Expect(listedTasks.Tasks).To(gomega.ContainElement(gomega.HaveField("ID", completedTask.ID))) + if expectPushSupported { + goClientAssertPushLifecycle(ctx, client, completedTask.ID) + } + pendingTask, err := goClientSendTask(ctx, client, pendingRequestText, true) gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(pendingTask.Status.State).To(gomega.Equal(a2a.TaskStateWorking)) @@ -546,6 +602,9 @@ func runRustProbe( if options.expectSubscribeUnsupported { args = append(args, "--expect-subscribe-unsupported") } + if options.expectPushSupported { + args = append(args, "--expect-push-supported") + } if options.expectPushUnsupported { args = append(args, "--expect-push-unsupported") if options.expectedPushErrorCode != 0 { @@ -653,7 +712,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("go", requestText))) - goClientAssertLifecycle(requestCtx, client, "go") + goClientAssertLifecycle(requestCtx, client, "go", false) }) ginkgo.It("lets the Go client call the Rust fixture", ginkgo.Label("jsonrpc", "go-rust"), func(ctx ginkgo.SpecContext) { @@ -671,7 +730,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) - goClientAssertLifecycle(requestCtx, client, "rust") + goClientAssertLifecycle(requestCtx, client, "rust", false) }) ginkgo.It("lets the Rust client call the Go fixture", ginkgo.Label("jsonrpc", "rust-go"), func(ctx ginkgo.SpecContext) { @@ -690,8 +749,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func defer cancel() output, err := runRustProbe(requestCtx, binaries, rustJSONRPCFixtureURL, "rust", rustProbeOptions{ - expectPushUnsupported: true, - expectedPushErrorCode: pushUnsupportedCode, + expectPushSupported: true, }) gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) }) @@ -713,7 +771,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("go", requestText))) - goClientAssertLifecycle(requestCtx, client, "go") + goClientAssertLifecycle(requestCtx, client, "go", false) }) ginkgo.It("lets the Go client call the Rust fixture over REST", ginkgo.Label("rest", "go-rust"), func(ctx ginkgo.SpecContext) { @@ -731,7 +789,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) - goClientAssertLifecycle(requestCtx, client, "rust") + goClientAssertLifecycle(requestCtx, client, "rust", false) }) ginkgo.It("lets the Rust client call the Go fixture over REST", ginkgo.Label("rest", "rust-go"), func(ctx ginkgo.SpecContext) { @@ -750,8 +808,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func defer cancel() output, err := runRustProbe(requestCtx, binaries, rustRESTFixtureURL, "rust", rustProbeOptions{ - expectPushUnsupported: true, - expectedPushErrorCode: pushUnsupportedCode, + expectPushSupported: true, }) gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) }) @@ -773,10 +830,10 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("go", requestText))) - goClientAssertLifecycle(requestCtx, client, "go") + goClientAssertLifecycle(requestCtx, client, "go", false) }) - ginkgo.It("documents the remaining Go client list_tasks mismatch against the Rust fixture over gRPC", ginkgo.Label("grpc", "go-rust"), func(ctx ginkgo.SpecContext) { + ginkgo.It("lets the Go client call the Rust fixture over gRPC", ginkgo.Label("grpc", "go-rust"), func(ctx ginkgo.SpecContext) { requestCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() @@ -791,28 +848,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) - completedTask, err := goClientSendTask(requestCtx, client, requestText, false) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(completedTask.ContextID).NotTo(gomega.BeEmpty()) - gomega.Expect(completedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) - - completedText, err := taskStatusText(completedTask) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(completedText).To(gomega.Equal(expectedServerText("rust", requestText))) - assertTaskHistoryPayload(completedTask, requestText, "completed task history") - - fetchedTask, err := client.GetTask(requestCtx, &a2a.GetTaskRequest{ID: completedTask.ID}) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(fetchedTask.Status.State).To(gomega.Equal(a2a.TaskStateCompleted)) - - fetchedText, err := taskStatusText(fetchedTask) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(fetchedText).To(gomega.Equal(expectedServerText("rust", requestText))) - assertTaskHistoryPayload(fetchedTask, requestText, "fetched task history") - - listedTasks, err := client.ListTasks(requestCtx, &a2a.ListTasksRequest{ContextID: completedTask.ContextID}) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(listedTasks.Tasks).To(gomega.BeEmpty()) + goClientAssertLifecycle(requestCtx, client, "rust", true) }) ginkgo.It("lets the Rust client call the Go fixture over gRPC", ginkgo.Label("grpc", "rust-go"), func(ctx ginkgo.SpecContext) { @@ -831,8 +867,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func defer cancel() output, err := runRustProbe(requestCtx, binaries, rustGRPCFixtureURL, "rust", rustProbeOptions{ - expectPushUnsupported: true, - expectedPushErrorCode: unsupportedOpCode, + expectPushSupported: true, }) gomega.Expect(err).NotTo(gomega.HaveOccurred(), output) }) From 4ab43e6e3879a2353c4a711c93f5dfc389d75743 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Mon, 6 Apr 2026 17:42:21 +0200 Subject: [PATCH 12/15] test(a2a): consume released Rust server 0.2.4 Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/fixtures/rust/Cargo.lock | 4 ++-- integrations/agntcy-a2a/fixtures/rust/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index 43704db1..4d3bf273 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-server" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68da8233159c97b16f9bc9c7b7ea15aa43e04738e4e519613f4c0b29500431f0" +checksum = "d91e52f121efea9622b201702690a96f9cb54bf64caf4c05ce790c67e72951ec" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index 9abb9801..c28297fa 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -9,7 +9,7 @@ a2a = { package = "agntcy-a2a", version = "0.2.4" } a2a-client = { package = "agntcy-a2a-client", version = "0.1.10" } a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.7" } a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.6" } -a2a-server = { package = "agntcy-a2a-server", version = "0.2.3" } +a2a-server = { package = "agntcy-a2a-server", version = "0.2.4" } axum = "0.8" futures = "0.3" serde_json = "1" From 879122303b5150b864360d868e65ec517467e3f9 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Mon, 6 Apr 2026 17:50:57 +0200 Subject: [PATCH 13/15] build(task): expose A2A integration test aliases Signed-off-by: Luca Muscariello --- integrations/Taskfile.yml | 85 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/integrations/Taskfile.yml b/integrations/Taskfile.yml index 8900cf11..b7d1f01b 100644 --- a/integrations/Taskfile.yml +++ b/integrations/Taskfile.yml @@ -59,5 +59,90 @@ tasks: cmds: - git describe --tags --match "v*" | cut -c 2- + test:a2a: + desc: All A2A interoperability tests + cmds: + - task: a2a:test + + test:a2a:rust-go: + desc: Rust and Go interoperability test matrix across JSON-RPC, HTTP+JSON, and gRPC + cmds: + - task: a2a:test:rust-go + + test:a2a:rust-go:jsonrpc: + desc: Rust and Go JSON-RPC interoperability matrix + cmds: + - task: a2a:test:rust-go:jsonrpc + + test:a2a:rust-go:rest: + desc: Rust and Go HTTP+JSON interoperability matrix + cmds: + - task: a2a:test:rust-go:rest + + test:a2a:rust-go:grpc: + desc: Rust and Go gRPC interoperability matrix + cmds: + - task: a2a:test:rust-go:grpc + + test:a2a:rust-go:jsonrpc:go-go: + desc: Go client to Go server JSON-RPC interoperability test + cmds: + - task: a2a:test:rust-go:jsonrpc:go-go + + test:a2a:rust-go:jsonrpc:go-rust: + desc: Go client to Rust server JSON-RPC interoperability test + cmds: + - task: a2a:test:rust-go:jsonrpc:go-rust + + test:a2a:rust-go:jsonrpc:rust-go: + desc: Rust client to Go server JSON-RPC interoperability test + cmds: + - task: a2a:test:rust-go:jsonrpc:rust-go + + test:a2a:rust-go:jsonrpc:rust-rust: + desc: Rust client to Rust server JSON-RPC interoperability test + cmds: + - task: a2a:test:rust-go:jsonrpc:rust-rust + + test:a2a:rust-go:rest:go-go: + desc: Go client to Go server HTTP+JSON interoperability test + cmds: + - task: a2a:test:rust-go:rest:go-go + + test:a2a:rust-go:rest:go-rust: + desc: Go client to Rust server HTTP+JSON interoperability test + cmds: + - task: a2a:test:rust-go:rest:go-rust + + test:a2a:rust-go:rest:rust-go: + desc: Rust client to Go server HTTP+JSON interoperability test + cmds: + - task: a2a:test:rust-go:rest:rust-go + + test:a2a:rust-go:rest:rust-rust: + desc: Rust client to Rust server HTTP+JSON interoperability test + cmds: + - task: a2a:test:rust-go:rest:rust-rust + + test:a2a:rust-go:grpc:go-go: + desc: Go client to Go server gRPC interoperability test + cmds: + - task: a2a:test:rust-go:grpc:go-go + + test:a2a:rust-go:grpc:go-rust: + desc: Go client to Rust server gRPC interoperability test + cmds: + - task: a2a:test:rust-go:grpc:go-rust + + test:a2a:rust-go:grpc:rust-go: + desc: Rust client to Go server gRPC interoperability test + cmds: + - task: a2a:test:rust-go:grpc:rust-go + + test:a2a:rust-go:grpc:rust-rust: + desc: Rust client to Rust server gRPC interoperability test + cmds: + - task: a2a:test:rust-go:grpc:rust-rust + default: cmd: task -l \ No newline at end of file From b279aa04e7a1132c7edfc31bf5ca5469cad9fdf2 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Mon, 6 Apr 2026 17:53:33 +0200 Subject: [PATCH 14/15] revert(task): drop duplicated A2A integration aliases Signed-off-by: Luca Muscariello --- integrations/Taskfile.yml | 85 --------------------------------------- 1 file changed, 85 deletions(-) diff --git a/integrations/Taskfile.yml b/integrations/Taskfile.yml index b7d1f01b..8900cf11 100644 --- a/integrations/Taskfile.yml +++ b/integrations/Taskfile.yml @@ -59,90 +59,5 @@ tasks: cmds: - git describe --tags --match "v*" | cut -c 2- - test:a2a: - desc: All A2A interoperability tests - cmds: - - task: a2a:test - - test:a2a:rust-go: - desc: Rust and Go interoperability test matrix across JSON-RPC, HTTP+JSON, and gRPC - cmds: - - task: a2a:test:rust-go - - test:a2a:rust-go:jsonrpc: - desc: Rust and Go JSON-RPC interoperability matrix - cmds: - - task: a2a:test:rust-go:jsonrpc - - test:a2a:rust-go:rest: - desc: Rust and Go HTTP+JSON interoperability matrix - cmds: - - task: a2a:test:rust-go:rest - - test:a2a:rust-go:grpc: - desc: Rust and Go gRPC interoperability matrix - cmds: - - task: a2a:test:rust-go:grpc - - test:a2a:rust-go:jsonrpc:go-go: - desc: Go client to Go server JSON-RPC interoperability test - cmds: - - task: a2a:test:rust-go:jsonrpc:go-go - - test:a2a:rust-go:jsonrpc:go-rust: - desc: Go client to Rust server JSON-RPC interoperability test - cmds: - - task: a2a:test:rust-go:jsonrpc:go-rust - - test:a2a:rust-go:jsonrpc:rust-go: - desc: Rust client to Go server JSON-RPC interoperability test - cmds: - - task: a2a:test:rust-go:jsonrpc:rust-go - - test:a2a:rust-go:jsonrpc:rust-rust: - desc: Rust client to Rust server JSON-RPC interoperability test - cmds: - - task: a2a:test:rust-go:jsonrpc:rust-rust - - test:a2a:rust-go:rest:go-go: - desc: Go client to Go server HTTP+JSON interoperability test - cmds: - - task: a2a:test:rust-go:rest:go-go - - test:a2a:rust-go:rest:go-rust: - desc: Go client to Rust server HTTP+JSON interoperability test - cmds: - - task: a2a:test:rust-go:rest:go-rust - - test:a2a:rust-go:rest:rust-go: - desc: Rust client to Go server HTTP+JSON interoperability test - cmds: - - task: a2a:test:rust-go:rest:rust-go - - test:a2a:rust-go:rest:rust-rust: - desc: Rust client to Rust server HTTP+JSON interoperability test - cmds: - - task: a2a:test:rust-go:rest:rust-rust - - test:a2a:rust-go:grpc:go-go: - desc: Go client to Go server gRPC interoperability test - cmds: - - task: a2a:test:rust-go:grpc:go-go - - test:a2a:rust-go:grpc:go-rust: - desc: Go client to Rust server gRPC interoperability test - cmds: - - task: a2a:test:rust-go:grpc:go-rust - - test:a2a:rust-go:grpc:rust-go: - desc: Rust client to Go server gRPC interoperability test - cmds: - - task: a2a:test:rust-go:grpc:rust-go - - test:a2a:rust-go:grpc:rust-rust: - desc: Rust client to Rust server gRPC interoperability test - cmds: - - task: a2a:test:rust-go:grpc:rust-rust - default: cmd: task -l \ No newline at end of file From 846c2f7b88df7084e8db4643c4391b16c285e1d9 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Mon, 6 Apr 2026 19:41:26 +0200 Subject: [PATCH 15/15] test(interop): promote go-rust push-config coverage Signed-off-by: Luca Muscariello --- integrations/agntcy-a2a/README.md | 6 ++---- integrations/agntcy-a2a/fixtures/rust/Cargo.lock | 12 ++++++------ integrations/agntcy-a2a/fixtures/rust/Cargo.toml | 6 +++--- .../agntcy-a2a/tests/interop_rust_go_test.go | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/integrations/agntcy-a2a/README.md b/integrations/agntcy-a2a/README.md index e760a916..4004143b 100644 --- a/integrations/agntcy-a2a/README.md +++ b/integrations/agntcy-a2a/README.md @@ -6,9 +6,7 @@ The current slice covers Rust and Go across the released JSON-RPC, HTTP+JSON, an All 12 Rust/Go client-server legs in the current core lifecycle matrix are green across JSON-RPC, HTTP+JSON, and gRPC. -The released Rust fixture now exposes push-config CRUD. CSIT validates that path from the Rust client across all three transports and from the Go client on the gRPC `go-rust` leg. - -Go client -> Rust server push-config creation over JSON-RPC and HTTP+JSON still has a request-shape mismatch, so those two transports remain outside the promoted push-config coverage. +The released Rust fixture now exposes push-config CRUD. CSIT validates that path from the Rust client across all three transports and from the Go client against Rust-server targets across JSON-RPC, HTTP+JSON, and gRPC. The Go fixture still models the current unsupported push-config behavior, and the Rust-probe scenarios keep validating that negative path against Go-server targets. @@ -17,7 +15,7 @@ Across the matrix, the scenarios validate the same core interoperability behavio - unary and streaming `SendMessage` - lifecycle methods across `GetTask`, `ListTasks`, and `CancelTask` - negative-path error semantics for missing and non-cancelable tasks -- successful push-config CRUD on the promoted Rust-server paths and unsupported push-config errors against the Go fixture where exercised +- successful push-config CRUD on the Rust-server paths across all three transports and unsupported push-config errors against the Go fixture where exercised - preservation of a mixed text plus structured-data request payload and message metadata through task history The fixtures are intentionally small and deterministic so the suite can run the same way locally and in CI without depending on sibling SDK checkouts. diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock index 4d3bf273..1487f192 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.lock +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-client" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1945dc35315d0d782f1ef3075cb2836370ad97e341c075f151d8954487588f67" +checksum = "5e8de5c7740a27a7d9c82f34ac1fbcc952f0d79d51d613254425c792ded08daa" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-grpc" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d17f8b8655b905db5a1a8f2384df2f06815b62fa50a34fa7e8693d2f6a7c841" +checksum = "08037695e3bfc8313e1695514dda97b2ef056a81fc9e5a875cd59fa9fd921484" dependencies = [ "agntcy-a2a", "agntcy-a2a-client", @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "agntcy-a2a-server" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e52f121efea9622b201702690a96f9cb54bf64caf4c05ce790c67e72951ec" +checksum = "d60b33664d32e509d7248902130973996e55b1d6b7de204497c1ff0b848fb621" dependencies = [ "agntcy-a2a", "agntcy-a2a-pb", diff --git a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml index c28297fa..6198ae70 100644 --- a/integrations/agntcy-a2a/fixtures/rust/Cargo.toml +++ b/integrations/agntcy-a2a/fixtures/rust/Cargo.toml @@ -6,10 +6,10 @@ publish = false [dependencies] a2a = { package = "agntcy-a2a", version = "0.2.4" } -a2a-client = { package = "agntcy-a2a-client", version = "0.1.10" } -a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.7" } +a2a-client = { package = "agntcy-a2a-client", version = "0.1.11" } +a2a-grpc = { package = "agntcy-a2a-grpc", version = "0.1.9" } a2a-pb = { package = "agntcy-a2a-pb", version = "0.1.6" } -a2a-server = { package = "agntcy-a2a-server", version = "0.2.4" } +a2a-server = { package = "agntcy-a2a-server", version = "0.2.5" } axum = "0.8" futures = "0.3" serde_json = "1" diff --git a/integrations/agntcy-a2a/tests/interop_rust_go_test.go b/integrations/agntcy-a2a/tests/interop_rust_go_test.go index 462a57f1..152d240c 100644 --- a/integrations/agntcy-a2a/tests/interop_rust_go_test.go +++ b/integrations/agntcy-a2a/tests/interop_rust_go_test.go @@ -730,7 +730,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) - goClientAssertLifecycle(requestCtx, client, "rust", false) + goClientAssertLifecycle(requestCtx, client, "rust", true) }) ginkgo.It("lets the Rust client call the Go fixture", ginkgo.Label("jsonrpc", "rust-go"), func(ctx ginkgo.SpecContext) { @@ -789,7 +789,7 @@ var _ = ginkgo.Describe("A2A Rust and Go interoperability", ginkgo.Ordered, func gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(streamText).To(gomega.Equal(expectedServerText("rust", requestText))) - goClientAssertLifecycle(requestCtx, client, "rust", false) + goClientAssertLifecycle(requestCtx, client, "rust", true) }) ginkgo.It("lets the Rust client call the Go fixture over REST", ginkgo.Label("rest", "rust-go"), func(ctx ginkgo.SpecContext) {