a2aproject / a2aproject/a2a-go

proposal: public a2atest testing package

未关闭
#310 5 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
Go
星标
460
派生
93
平均合并
2 天 21 小时
30 天内合并 PR
9

描述

## Proposal: Public `a2atest` package for SDK consumers

### Motivation

The a2a-go SDK has excellent internal test infrastructure in `internal/testutil/`
and `internal/testutil/testexecutor/`. However, these packages are not accessible
to SDK consumers, which means everyone building A2A agents in Go must create
their own test mocks, in-process servers, and assertion helpers from scratch.

This is the **single most common pain point** reported by Go SDK adopters. The
Python SDK provides `a2a.testing` with mock servers and test clients. The JS SDK
has similar utilities. The Go SDK is the only major A2A SDK without public test
helpers.

### Current internal test infrastructure

The SDK already has well-designed test utilities that cover the core testing needs:

| Internal package | Contents | Consumers |
|---|---|---|
| `internal/testutil` | `TestTaskStore`, `TestPushConfigStore`, `TestPushSender`, `TestEventQueue`, `TestQueueManager`, `TestWorkQueue`, test logger | `a2asrv` handler tests, `internal/taskexec` tests |
| `internal/testutil/testexecutor` | `TestAgentExecutor` with control channels, event generators, functional constructors | e2e tests, handler tests |

These utilities follow a consistent pattern: they embed the real implementation
and allow individual method overrides via function fields. This is a clean,
composable design that SDK consumers would benefit from directly.

### Proposed public API

A new top-level `a2atest` package (following the convention of `a2aclient`,
`a2asrv`, `a2aext`):

```go
package a2atest

// --- In-process test server ---

// Server is an in-process A2A server for integration testing.
// It starts an httptest.Server with JSON-RPC and REST handlers wired up.
type Server struct { /* ... */ }

// NewServer creates an in-process A2A test server with the given executor
// and options. The server is automatically cleaned up when the test finishes.
func NewServer(t testing.TB, executor a2asrv.AgentExecutor, opts ...ServerOption) *Server

// URL returns the base URL of the test server.
func (s *Server) URL() string

// Card returns the AgentCard for the test server.
func (s *Server) Card() *a2a.AgentCard

// Client returns a pre-configured client connected to the test server.
func (s *Server) Client(t testing.TB) *a2aclient.Client

// Handler returns the underlying RequestHandler for direct handler testing.
func (s *Server) Handler() a2asrv.RequestHandler

// ServerOption configures the test server.
type ServerOption func(*serverConfig)

// WithTaskStore configures the test server with a custom task store.
func WithTaskStore(store taskstore.Store) ServerOption

// WithCapabilities sets the agent capabilities on the test server's AgentCard.
func WithCapabilities(caps a2a.AgentCapabilities) ServerOption

// WithSkills sets the skills on the test server's AgentCard.
func WithSkills(skills []a2a.AgentSkill) ServerOption

// WithCallInterceptors adds call interceptors to the test server.
func WithCallInterceptors(interceptors ...a2asrv.CallInterceptor) ServerOption

// --- Mock executor ---

// Executor is a configurable mock AgentExecutor for testing.
// It embeds the internal TestAgentExecutor functionality.
type Executor struct { /* ... */ }

// NewExecutor creates a mock executor. Without configuration, it returns
// an empty event sequence.
func NewExecutor() *Executor

// FromFunc creates an Executor from a function.
func FromFunc(fn func(ctx context.Context, ec *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error]) *Executor

// FromEvents creates an Executor that emits a fixed sequence of events.
func FromEvents(fn func(execCtx *a2asrv.ExecutorContext) []a2a.Event) *Executor

// Emitted returns all events emitted by the executor (thread-safe).
func (e *Executor) Emitted() []a2a.Event

// WithControlChannels returns an Executor with channels for controlling
// execution timing in race condition tests.
func WithControlChannels() (*Executor, *ControlChannels)

// ControlChannels provides channels for controlling executor behavior.
type ControlChannels struct {
ReqCtx <-chan *a2asrv.ExecutorContext
ExecEvent chan<- a2a.Event
CancelCalled <-chan struct{}
ContinueCancel chan<- struct{}
}

// --- Mock task store ---

// TaskStore is a mock task store that delegates to InMemory by default.
// Individual operations can be overridden.
type TaskStore struct { /* ... */ }

// NewTaskStore creates a mock task store backed by in-memory storage.
func NewTaskStore() *TaskStore

// SetSaveError overrides Create and Update to return the given error.
func (s *TaskStore) SetSaveError(err error) *TaskStore

// SetGetOverride overrides Get to return the given task and error.
func (s *TaskStore) SetGetOverride(task *taskstore.StoredTask, err error) *TaskStore

// WithTasks seeds the store with the given tasks.
func (s *TaskStore) WithTasks(t testing.TB, tasks ...*a2a.Task) *TaskStore

// --- Test logger ---

// NewLogger returns an slog.Logger that directs output to t.Log().
// Output is only printed for failed tests or when running with -v.
func NewLogger(t testing.TB) *slog.Logger

// SetDefaultLogger calls slog.SetDefault with a test logger and restores
// the original logger on test cleanup.
func SetDefaultLogger(t testing.TB)
```

### Design principles

1. **Export what already works**: The internal test infrastructure is battle-tested.
The public API should be a thin wrapper, not a rewrite.
2. **Zero configuration for common cases**: `a2atest.NewServer(t, executor)` should
work with no options for the 80% case.
3. **Composable for advanced cases**: Options for custom stores, interceptors,
and capabilities cover the remaining 20%.
4. **Test cleanup via `t.Cleanup()`**: All resources are automatically cleaned up
when the test finishes. No manual `defer server.Close()` needed.
5. **Follow the SDK's own patterns**: Use `t.Helper()`, `cmp.Diff()`,
`t.Parallel()`, and the same error formatting conventions documented in AGENTS.md.

### Relationship to existing internal packages

The `a2atest` package would **import from** `internal/testutil` initially via
package-level delegation, then gradually replace the internal package as the
public API stabilizes. The internal package can eventually become a thin re-export
or be removed entirely once all internal tests migrate to the public API.

Alternatively, if the maintainers prefer, the internal utilities can be moved
directly to the public package in a single refactor. I am happy to go either
direction.

### Implementation plan

I am happy to implement this. Proposed approach:

**Phase 1** (1 PR): Create `a2atest/` package with:
- `Server` (in-process server with JSON-RPC handler)
- `Executor` (wrapping `testexecutor.TestAgentExecutor`)
- `TaskStore` (wrapping `testutil.TestTaskStore`)
- `NewLogger` and `SetDefaultLogger`
- Tests and `doc.go` with package documentation
- Example test showing the full pattern

**Phase 2** (follow-up PR): Add:
- REST transport support in test server
- Assertion helpers (`AssertTaskState`, `AssertArtifacts`, `AssertEvents`)
- Push notification test infrastructure (`PushConfigStore`, `PushSender`)
- Migration of select internal tests to use the public API

**Phase 3** (optional, based on feedback): Add:
- gRPC transport support in test server
- Test fixtures (pre-built AgentCards, common event sequences)
- Benchmarking helpers

### Backward compatibility

This is a purely additive change. No existing APIs are modified. The new package
can be imported independently:

```go
import "github.com/a2aproject/a2a-go/v2/a2atest"
```

### Context

I have an open PR (#309) fixing a race condition in task execution cleanup, which gave me familiarity with the internal test infrastructure and motivated this proposal.

### Questions for maintainers

1. Does this align with your vision for the SDK's public API surface?
2. Do you prefer the public package to delegate to `internal/testutil` or to
directly move the internal code?
3. Are there additional test utilities you've wished existed when writing the
SDK's own tests?
4. Any naming preferences? (`a2atest` vs `testutil` vs `testing`)

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。