micro--go-micro
4311b73361
* docs: compare Go Micro with Google ADK in the comparison guide Adds a 'vs Agent Frameworks (Google ADK)' section: ADK builds an agent, Go Micro builds the distributed system the agent lives in (agents are services in the mesh). Covers the category difference, a feature table, when to choose each, and MCP/A2A interoperability. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL * docs: replace ADK comparison slogan with concrete explanation State plainly what each tool provides (ADK builds an agent process; Go Micro builds the surrounding service mesh) instead of marketing phrasing. * lint: apply golangci-lint autofixes; exclude ST1003 and demo errcheck Mechanical, behaviour-preserving fixes applied by 'golangci-lint run --fix': gofmt, misspell (US spelling), usestdlibvars (http.Method*/Status*), unconvert, and the auto-fixable staticcheck simplifications (QF*, S1017/S1019/S1023/S1039). Config: exclude ST1003 (remaining offenders are exported API renames, e.g. web.Id, which would break compatibility) and skip errcheck for examples/ and internal/harness/ (demo code where fire-and-forget is intentional). Build and test compilation verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL * lint: WIP cleanup checkpoint (errcheck config + partial fixes) Checkpoint of an in-progress golangci-lint cleanup (background pass). Builds cleanly; lint is not yet zero. Follow-up commit will complete the cleanup and switch CI to a blocking full-tree lint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL --------- Co-authored-by: Claude <noreply@anthropic.com>
167 行
4.5 KiB
Go
167 行
4.5 KiB
Go
package a2a
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
pb "go-micro.dev/v6/agent/proto"
|
|
"go-micro.dev/v6/client"
|
|
"go-micro.dev/v6/registry"
|
|
"go-micro.dev/v6/selector"
|
|
"go-micro.dev/v6/server"
|
|
)
|
|
|
|
// echoAgent is a stub that implements the Agent proto handler — enough to
|
|
// exercise the gateway's task→Agent.Chat translation without pulling in
|
|
// the agent package (which would import this one, a test-only cycle).
|
|
type echoAgent struct{}
|
|
|
|
func (echoAgent) Chat(_ context.Context, req *pb.ChatRequest, rsp *pb.ChatResponse) error {
|
|
rsp.Reply = "pong"
|
|
rsp.Agent = "echo"
|
|
return nil
|
|
}
|
|
|
|
func waitFor(reg registry.Registry, name string) {
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if svcs, err := reg.GetService(name); err == nil && len(svcs) > 0 && len(svcs[0].Nodes) > 0 {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func newGatewayWithAgent(t *testing.T) (*httptest.Server, func()) {
|
|
t.Helper()
|
|
reg := registry.NewMemoryRegistry()
|
|
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))))
|
|
|
|
srv := server.NewServer(
|
|
server.Name("echo"),
|
|
server.Registry(reg),
|
|
server.Metadata(map[string]string{"type": "agent", "services": ""}),
|
|
)
|
|
if err := pb.RegisterAgentHandler(srv, echoAgent{}); err != nil {
|
|
t.Fatalf("register agent handler: %v", err)
|
|
}
|
|
if err := srv.Start(); err != nil {
|
|
t.Fatalf("start server: %v", err)
|
|
}
|
|
waitFor(reg, "echo")
|
|
|
|
g := New(Options{Registry: reg, Client: cl, BaseURL: "http://gw"})
|
|
ts := httptest.NewServer(g.Handler())
|
|
return ts, func() { ts.Close(); srv.Stop() }
|
|
}
|
|
|
|
func TestAgentCardFromRegistry(t *testing.T) {
|
|
ts, cleanup := newGatewayWithAgent(t)
|
|
defer cleanup()
|
|
|
|
resp, err := http.Get(ts.URL + "/agents/echo/.well-known/agent.json")
|
|
if err != nil {
|
|
t.Fatalf("get card: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("card status = %d", resp.StatusCode)
|
|
}
|
|
var card AgentCard
|
|
if err := json.NewDecoder(resp.Body).Decode(&card); err != nil {
|
|
t.Fatalf("decode card: %v", err)
|
|
}
|
|
if card.Name != "echo" {
|
|
t.Errorf("card name = %q, want echo", card.Name)
|
|
}
|
|
if card.URL != "http://gw/agents/echo" {
|
|
t.Errorf("card url = %q", card.URL)
|
|
}
|
|
if card.ProtocolVersion == "" || len(card.Skills) == 0 {
|
|
t.Errorf("card missing protocolVersion or skills: %+v", card)
|
|
}
|
|
}
|
|
|
|
func TestMessageSendAndGet(t *testing.T) {
|
|
ts, cleanup := newGatewayWithAgent(t)
|
|
defer cleanup()
|
|
|
|
task := rpcTask(t, ts.URL+"/agents/echo", `{
|
|
"jsonrpc":"2.0","id":1,"method":"message/send",
|
|
"params":{"message":{"role":"user","kind":"message","messageId":"m1",
|
|
"parts":[{"kind":"text","text":"ping"}]}}}`)
|
|
if task.Status.State != stateCompleted {
|
|
t.Fatalf("task state = %q, want completed", task.Status.State)
|
|
}
|
|
if len(task.Artifacts) != 1 || textOf(task.Artifacts[0].Parts) != "pong" {
|
|
t.Fatalf("artifact = %+v, want text 'pong'", task.Artifacts)
|
|
}
|
|
|
|
got := rpcTask(t, ts.URL+"/agents/echo", `{
|
|
"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"id":"`+task.ID+`"}}`)
|
|
if got.ID != task.ID || got.Status.State != stateCompleted {
|
|
t.Errorf("tasks/get returned %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestUnknownMethod(t *testing.T) {
|
|
ts, cleanup := newGatewayWithAgent(t)
|
|
defer cleanup()
|
|
|
|
var resp struct {
|
|
Error *rpcError `json:"error"`
|
|
}
|
|
rpc(t, ts.URL+"/agents/echo", `{"jsonrpc":"2.0","id":1,"method":"message/stream","params":{}}`, &resp)
|
|
if resp.Error == nil || resp.Error.Code != errMethodNotFound {
|
|
t.Errorf("expected method-not-found for streaming, got %+v", resp.Error)
|
|
}
|
|
}
|
|
|
|
func TestListAgents(t *testing.T) {
|
|
ts, cleanup := newGatewayWithAgent(t)
|
|
defer cleanup()
|
|
|
|
resp, err := http.Get(ts.URL + "/agents")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var out struct {
|
|
Agents []AgentCard `json:"agents"`
|
|
}
|
|
json.NewDecoder(resp.Body).Decode(&out)
|
|
if len(out.Agents) != 1 || out.Agents[0].Name != "echo" {
|
|
t.Errorf("agents list = %+v", out.Agents)
|
|
}
|
|
}
|
|
|
|
func rpc(t *testing.T, url, body string, v any) {
|
|
t.Helper()
|
|
resp, err := http.Post(url, "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
}
|
|
|
|
func rpcTask(t *testing.T, url, body string) Task {
|
|
t.Helper()
|
|
var resp struct {
|
|
Result Task `json:"result"`
|
|
Error *rpcError `json:"error"`
|
|
}
|
|
rpc(t, url, body, &resp)
|
|
if resp.Error != nil {
|
|
t.Fatalf("rpc error: %+v", resp.Error)
|
|
}
|
|
return resp.Result
|
|
}
|