Skip to content

Commit 7888e37

Browse files
docs(a2asrv): add Example_* test functions for pkg.go.dev documentation (#262)
## Description Ref #257 🦕 ### Motivation The `a2asrv/` server package does not include testable examples (`Example_*` functions). Usage snippets exist in the README and `doc.go`, but they are not executable, not validated by `go test`, and do not render as function-level examples on **pkg.go.dev**. ### Changes Add `a2asrv/example_test.go` using `package a2asrv_test` (external test package), following Go's `ExampleXxx` / `ExampleType_Method` naming convention. Examples added for the following public API surface: | Function / Type | Example demonstrates | |---|---| | `NewHandler` | Creating a basic request handler with an `AgentExecutor` | | `NewHandler` (withOptions) | Creating a handler with `WithExtendedAgentCard` and `WithCallInterceptors` | | `NewJSONRPCHandler` | Wrapping a handler with JSON-RPC transport and registering with `http.ServeMux` | | `NewStaticAgentCardHandler` | Serving a static `AgentCard` via httptest and verifying JSON response | | `NewAgentCardHandler` | Serving a dynamic `AgentCard` via an `AgentCardProducerFn` | | `WellKnownAgentCardPath` | Displaying the standard well-known path constant | | `WithCallInterceptors` | Adding server-side middleware to a handler | | `PassthroughCallInterceptor` | Demonstrating the no-op `Before`/`After` interceptor lifecycle | | `NewCallContext` | Creating a call context with `ServiceParams` and reading request metadata | | `NewServiceParams` | Case-insensitive service parameter lookups | | `CallContext.Extensions` | Requesting, activating, and inspecting extensions | All examples include `// Output:` comments and are validated by `go test`. ### Tests The examples themselves are tests — validated through output matching. All 12 pass. Existing tests are unaffected. ``` go test ./a2asrv/ -v -run Example ``` ### Additional context Import paths follow the current module declaration (`github.com/a2aproject/a2a-go`), consistent with the temporary revert in #254 (ref #250). Happy to update if the module path changes. This is the first of two PRs addressing #257. The second PR will add examples for `a2aclient/`. --------- Co-authored-by: Yaroslav <yarolegovich@gmail.com>
1 parent b1f055c commit 7888e37

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

a2asrv/example_test.go

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
// Copyright 2025 The A2A Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package a2asrv_test
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
"iter"
22+
"net/http"
23+
"net/http/httptest"
24+
"strings"
25+
26+
"github.com/a2aproject/a2a-go/v2/a2a"
27+
"github.com/a2aproject/a2a-go/v2/a2asrv"
28+
)
29+
30+
type echoExecutor struct {
31+
ExecuteFn func(context.Context, *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error]
32+
}
33+
34+
func (e *echoExecutor) Execute(ctx context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] {
35+
if e.ExecuteFn != nil {
36+
return e.ExecuteFn(ctx, execCtx)
37+
}
38+
return func(yield func(a2a.Event, error) bool) {
39+
yield(a2a.NewMessage(a2a.MessageRoleAgent, a2a.NewTextPart("echo")), nil)
40+
}
41+
}
42+
43+
func (e *echoExecutor) Cancel(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] {
44+
return func(yield func(a2a.Event, error) bool) {
45+
yield(a2a.NewStatusUpdateEvent(execCtx, a2a.TaskStateCanceled, nil), nil)
46+
}
47+
}
48+
49+
type testInterceptor struct {
50+
BeforeFn func(ctx context.Context, callCtx *a2asrv.CallContext, req *a2asrv.Request) (context.Context, any, error)
51+
AfterFn func(ctx context.Context, callCtx *a2asrv.CallContext, resp *a2asrv.Response) error
52+
}
53+
54+
func (ti *testInterceptor) Before(ctx context.Context, callCtx *a2asrv.CallContext, req *a2asrv.Request) (context.Context, any, error) {
55+
if ti.BeforeFn != nil {
56+
return ti.BeforeFn(ctx, callCtx, req)
57+
}
58+
return ctx, nil, nil
59+
}
60+
61+
func (ti *testInterceptor) After(ctx context.Context, callCtx *a2asrv.CallContext, resp *a2asrv.Response) error {
62+
if ti.AfterFn != nil {
63+
return ti.AfterFn(ctx, callCtx, resp)
64+
}
65+
return nil
66+
}
67+
68+
func ExampleNewHandler() {
69+
executor := &echoExecutor{}
70+
handler := a2asrv.NewHandler(executor)
71+
72+
fmt.Println("Handler created:", handler != nil)
73+
// Output:
74+
// Handler created: true
75+
}
76+
77+
func ExampleNewHandler_withOptions() {
78+
executor := &echoExecutor{}
79+
handler := a2asrv.NewHandler(
80+
executor,
81+
a2asrv.WithExtendedAgentCard(&a2a.AgentCard{Name: "Extended Agent"}),
82+
a2asrv.WithCallInterceptors(&a2asrv.PassthroughCallInterceptor{}),
83+
)
84+
85+
fmt.Println("Handler with options created:", handler != nil)
86+
// Output:
87+
// Handler with options created: true
88+
}
89+
90+
func ExampleNewJSONRPCHandler() {
91+
executor := &echoExecutor{}
92+
handler := a2asrv.NewHandler(executor)
93+
jsonrpcHandler := a2asrv.NewJSONRPCHandler(handler)
94+
95+
mux := http.NewServeMux()
96+
mux.Handle("/", jsonrpcHandler)
97+
98+
fmt.Println("JSON-RPC handler registered:", jsonrpcHandler != nil)
99+
// Output:
100+
// JSON-RPC handler registered: true
101+
}
102+
103+
func ExampleNewStaticAgentCardHandler() {
104+
card := &a2a.AgentCard{
105+
Name: "Echo Agent",
106+
Version: "1.0.0",
107+
SupportedInterfaces: []*a2a.AgentInterface{
108+
a2a.NewAgentInterface("http://localhost:8080", a2a.TransportProtocolJSONRPC),
109+
},
110+
}
111+
handler := a2asrv.NewStaticAgentCardHandler(card)
112+
113+
server := httptest.NewServer(handler)
114+
defer server.Close()
115+
116+
resp, err := http.Get(server.URL)
117+
if err != nil {
118+
fmt.Println("Error:", err)
119+
return
120+
}
121+
defer func() { _ = resp.Body.Close() }()
122+
123+
var result map[string]any
124+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
125+
fmt.Println("Error:", err)
126+
return
127+
}
128+
129+
fmt.Println("Name:", result["name"])
130+
fmt.Println("Version:", result["version"])
131+
fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
132+
// Output:
133+
// Name: Echo Agent
134+
// Version: 1.0.0
135+
// Content-Type: application/json
136+
}
137+
138+
func ExampleNewAgentCardHandler() {
139+
producer := a2asrv.AgentCardProducerFn(func(_ context.Context) (*a2a.AgentCard, error) {
140+
return &a2a.AgentCard{
141+
Name: "Dynamic Agent",
142+
Version: "2.0.0",
143+
SupportedInterfaces: []*a2a.AgentInterface{
144+
a2a.NewAgentInterface("http://localhost:8080", a2a.TransportProtocolJSONRPC),
145+
},
146+
}, nil
147+
})
148+
handler := a2asrv.NewAgentCardHandler(producer)
149+
150+
server := httptest.NewServer(handler)
151+
defer server.Close()
152+
153+
resp, err := http.Get(server.URL)
154+
if err != nil {
155+
fmt.Println("Error:", err)
156+
return
157+
}
158+
defer func() { _ = resp.Body.Close() }()
159+
160+
var result map[string]any
161+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
162+
fmt.Println("Error:", err)
163+
return
164+
}
165+
166+
fmt.Println("Name:", result["name"])
167+
fmt.Println("Version:", result["version"])
168+
// Output:
169+
// Name: Dynamic Agent
170+
// Version: 2.0.0
171+
}
172+
173+
func ExamplePassthroughCallInterceptor() {
174+
type myInterceptor struct {
175+
a2asrv.PassthroughCallInterceptor
176+
}
177+
178+
handler := a2asrv.NewHandler(&echoExecutor{}, a2asrv.WithCallInterceptors(myInterceptor{}))
179+
fmt.Println("Handler created:", handler != nil)
180+
// Output:
181+
// Handler created: true
182+
}
183+
184+
func ExampleUser() {
185+
authenticate := func(_ string) string { return "user" }
186+
187+
interceptor := &testInterceptor{
188+
BeforeFn: func(ctx context.Context, callCtx *a2asrv.CallContext, req *a2asrv.Request) (context.Context, any, error) {
189+
if auth, ok := callCtx.ServiceParams().Get("authorization"); ok && len(auth) > 0 && strings.HasPrefix(auth[0], "Bearer ") {
190+
if name := authenticate(auth[0]); name != "" {
191+
callCtx.User = a2asrv.NewAuthenticatedUser(name, nil)
192+
}
193+
}
194+
return ctx, nil, nil
195+
},
196+
}
197+
198+
executor := &echoExecutor{
199+
ExecuteFn: func(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] {
200+
return func(yield func(a2a.Event, error) bool) {
201+
fmt.Println("Auth found:", execCtx.User.Name)
202+
yield(a2a.NewMessage(a2a.MessageRoleAgent, a2a.NewTextPart("echo")), nil)
203+
}
204+
},
205+
}
206+
207+
ctx, _ := a2asrv.NewCallContext(context.Background(), a2asrv.NewServiceParams(map[string][]string{
208+
"Authorization": {"Bearer token"},
209+
}))
210+
handler := a2asrv.NewHandler(executor, a2asrv.WithCallInterceptors(interceptor))
211+
_, err := handler.SendMessage(ctx, &a2a.SendMessageRequest{
212+
Message: a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart("echo")),
213+
})
214+
fmt.Println("Error:", err)
215+
// Output:
216+
// Auth found: user
217+
// Error: <nil>
218+
}
219+
220+
func ExampleAgentExecutor() {
221+
executor := &echoExecutor{
222+
ExecuteFn: func(_ context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] {
223+
return func(yield func(a2a.Event, error) bool) {
224+
if !yield(a2a.NewSubmittedTask(execCtx, execCtx.Message), nil) {
225+
return
226+
}
227+
228+
if !yield(a2a.NewStatusUpdateEvent(execCtx, a2a.TaskStateWorking, nil), nil) {
229+
return
230+
}
231+
232+
if !yield(a2a.NewArtifactEvent(execCtx, a2a.NewTextPart("generated output")), nil) {
233+
return
234+
}
235+
236+
yield(a2a.NewStatusUpdateEvent(execCtx, a2a.TaskStateCompleted, nil), nil)
237+
}
238+
},
239+
}
240+
241+
handler := a2asrv.NewHandler(executor)
242+
243+
msg := a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart("generate something"))
244+
result, err := handler.SendMessage(context.Background(), &a2a.SendMessageRequest{Message: msg})
245+
if err != nil {
246+
fmt.Println("Error:", err)
247+
return
248+
}
249+
250+
task, ok := result.(*a2a.Task)
251+
if !ok {
252+
fmt.Println("Expected task result")
253+
return
254+
}
255+
256+
fmt.Println("State:", task.Status.State)
257+
fmt.Println("Artifacts:", len(task.Artifacts))
258+
// Output:
259+
// State: TASK_STATE_COMPLETED
260+
// Artifacts: 1
261+
}
262+
263+
func ExampleNewHandler_fullServer() {
264+
executor := &echoExecutor{}
265+
handler := a2asrv.NewHandler(executor)
266+
267+
card := &a2a.AgentCard{
268+
Name: "My Agent",
269+
Version: "1.0.0",
270+
SupportedInterfaces: []*a2a.AgentInterface{
271+
a2a.NewAgentInterface("http://localhost:8080", a2a.TransportProtocolJSONRPC),
272+
},
273+
}
274+
275+
mux := http.NewServeMux()
276+
mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(card))
277+
mux.Handle("/", a2asrv.NewJSONRPCHandler(handler))
278+
279+
fmt.Println("Agent card path:", a2asrv.WellKnownAgentCardPath)
280+
fmt.Println("Server ready")
281+
// Output:
282+
// Agent card path: /.well-known/agent-card.json
283+
// Server ready
284+
}
285+
286+
func ExampleServiceParams() {
287+
var capturedHeader string
288+
289+
interceptor := &testInterceptor{
290+
BeforeFn: func(ctx context.Context, callCtx *a2asrv.CallContext, _ *a2asrv.Request) (context.Context, any, error) {
291+
if vals, ok := callCtx.ServiceParams().Get("x-custom-header"); ok && len(vals) > 0 {
292+
capturedHeader = vals[0]
293+
}
294+
return ctx, nil, nil
295+
},
296+
}
297+
298+
executor := &echoExecutor{}
299+
handler := a2asrv.NewHandler(executor, a2asrv.WithCallInterceptors(interceptor))
300+
restHandler := a2asrv.NewRESTHandler(handler)
301+
302+
server := httptest.NewServer(restHandler)
303+
defer server.Close()
304+
305+
req, _ := http.NewRequest("GET", server.URL+"/tasks/task-123", nil)
306+
req.Header.Set("X-Custom-Header", "my-value")
307+
308+
resp, err := http.DefaultClient.Do(req)
309+
if err != nil {
310+
fmt.Println("Error:", err)
311+
return
312+
}
313+
defer func() { _ = resp.Body.Close() }()
314+
315+
// The task won't be found, but the interceptor still captures the header.
316+
fmt.Println("Header in ServiceParams:", capturedHeader)
317+
// Output:
318+
// Header in ServiceParams: my-value
319+
}

0 commit comments

Comments
 (0)