diff --git a/a2a/auth.go b/a2a/auth.go index 8b518494..f53a16dd 100644 --- a/a2a/auth.go +++ b/a2a/auth.go @@ -45,24 +45,22 @@ type SecurityRequirements map[SecuritySchemeName]SecuritySchemeScopes // } type SecurityRequirementsOptions []SecurityRequirements +type securityRequirements struct { + Schemes map[SecuritySchemeName]SecuritySchemeScopes `json:"schemes"` +} + // MarshalJSON implements json.Marshaler. func (rs SecurityRequirementsOptions) MarshalJSON() ([]byte, error) { - type wrapper struct { - Schemes map[SecuritySchemeName]SecuritySchemeScopes `json:"schemes"` - } - var out []wrapper + var out []securityRequirements for _, req := range rs { - out = append(out, wrapper{Schemes: req}) + out = append(out, securityRequirements{Schemes: req}) } return json.Marshal(out) } // UnmarshalJSON implements json.Unmarshaler. func (rs *SecurityRequirementsOptions) UnmarshalJSON(b []byte) error { - type wrapper struct { - Schemes map[SecuritySchemeName]SecuritySchemeScopes `json:"schemes"` - } - var wrapped []wrapper + var wrapped []securityRequirements if err := json.Unmarshal(b, &wrapped); err != nil { return err } @@ -85,80 +83,76 @@ type SecuritySchemeScopes []string // The key is the scheme name. Follows the OpenAPI 3.0 Security Scheme Object. type NamedSecuritySchemes map[SecuritySchemeName]SecurityScheme +type securityScheme struct { + APIKey *APIKeySecurityScheme `json:"apiKeySecurityScheme,omitempty"` + HTTPAuth *HTTPAuthSecurityScheme `json:"httpAuthSecurityScheme,omitempty"` + MutualTLS *MutualTLSSecurityScheme `json:"mtlsSecurityScheme,omitempty"` + OAuth2 *OAuth2SecurityScheme `json:"oauth2SecurityScheme,omitempty"` + OpenIDConnect *OpenIDConnectSecurityScheme `json:"openIdConnectSecurityScheme,omitempty"` +} + // MarshalJSON implements json.Marshaler. func (s NamedSecuritySchemes) MarshalJSON() ([]byte, error) { - out := make(map[SecuritySchemeName]any) + out := make(map[SecuritySchemeName]securityScheme, len(s)) for name, scheme := range s { - var wrapped any + var wrapper securityScheme switch v := scheme.(type) { - // TODO: remove short JSON discriminator keys after transition period case APIKeySecurityScheme: - wrapped = map[string]any{"apiKeySecurityScheme": v} + wrapper.APIKey = &v case HTTPAuthSecurityScheme: - wrapped = map[string]any{"httpAuthSecurityScheme": v} + wrapper.HTTPAuth = &v case OpenIDConnectSecurityScheme: - wrapped = map[string]any{"openIdConnectSecurityScheme": v} + wrapper.OpenIDConnect = &v case MutualTLSSecurityScheme: - wrapped = map[string]any{"mtlsSecurityScheme": v} + wrapper.MutualTLS = &v case OAuth2SecurityScheme: - wrapped = map[string]any{"oauth2SecurityScheme": v} + wrapper.OAuth2 = &v default: return nil, fmt.Errorf("unknown security scheme type %T", v) } - out[name] = wrapped + out[name] = wrapper } return json.Marshal(out) } // UnmarshalJSON implements json.Unmarshaler. func (s *NamedSecuritySchemes) UnmarshalJSON(b []byte) error { - var schemes map[SecuritySchemeName]json.RawMessage + var schemes map[SecuritySchemeName]securityScheme if err := json.Unmarshal(b, &schemes); err != nil { return err } - - result := make(map[SecuritySchemeName]SecurityScheme, len(schemes)) - for name, rawMessage := range schemes { - var raw map[string]json.RawMessage - if err := json.Unmarshal(rawMessage, &raw); err != nil { - return err + result := make(NamedSecuritySchemes, len(schemes)) + for name, wrapper := range schemes { + var n int + if wrapper.APIKey != nil { + result[name] = *wrapper.APIKey + n++ } - if v, ok := raw["apiKeySecurityScheme"]; ok { - var scheme APIKeySecurityScheme - if err := json.Unmarshal(v, &scheme); err != nil { - return err - } - result[name] = scheme - } else if v, ok := raw["httpAuthSecurityScheme"]; ok { - var scheme HTTPAuthSecurityScheme - if err := json.Unmarshal(v, &scheme); err != nil { - return err - } - result[name] = scheme - } else if v, ok := raw["mtlsSecurityScheme"]; ok { - var scheme MutualTLSSecurityScheme - if err := json.Unmarshal(v, &scheme); err != nil { - return err - } - result[name] = scheme - } else if v, ok := raw["oauth2SecurityScheme"]; ok { - var scheme OAuth2SecurityScheme - if err := json.Unmarshal(v, &scheme); err != nil { - return err - } - result[name] = scheme - } else if v, ok := raw["openIdConnectSecurityScheme"]; ok { - var scheme OpenIDConnectSecurityScheme - if err := json.Unmarshal(v, &scheme); err != nil { - return err - } - result[name] = scheme - } else { - keys := make([]string, 0, len(raw)) - for k := range raw { - keys = append(keys, k) + if wrapper.HTTPAuth != nil { + result[name] = *wrapper.HTTPAuth + n++ + } + if wrapper.OpenIDConnect != nil { + result[name] = *wrapper.OpenIDConnect + n++ + } + if wrapper.MutualTLS != nil { + result[name] = *wrapper.MutualTLS + n++ + } + if wrapper.OAuth2 != nil { + result[name] = *wrapper.OAuth2 + n++ + } + if n == 0 { + var raw map[SecuritySchemeName]json.RawMessage + if err := json.Unmarshal(b, &raw); err != nil { + return fmt.Errorf("unknown security scheme for %s", name) } - return fmt.Errorf("unknown security scheme type for %q: found keys %v", name, keys) + return fmt.Errorf("unknown security scheme type for %s: %v", name, jsonKeys([]byte(raw[name]))) + } + if n != 1 { + return fmt.Errorf("expected exactly one security scheme type for %s, got %d", name, n) } } @@ -268,25 +262,34 @@ const ( DeviceCodeOAuthFlowName OAuthFlowName = "deviceCode" ) +type oauth2 struct { + Description string `json:"description,omitempty"` + Oauth2MetadataURL string `json:"oauth2MetadataUrl,omitempty"` + Flows oauthFlows `json:"flows"` +} + +type oauthFlows struct { + AuthorizationCode *AuthorizationCodeOAuthFlow `json:"authorizationCode,omitempty"` + ClientCredentials *ClientCredentialsOAuthFlow `json:"clientCredentials,omitempty"` + Implicit *ImplicitOAuthFlow `json:"implicit,omitempty"` + Password *PasswordOAuthFlow `json:"password,omitempty"` + DeviceCode *DeviceCodeOAuthFlow `json:"deviceCode,omitempty"` +} + // MarshalJSON implements json.Marshaler. func (s OAuth2SecurityScheme) MarshalJSON() ([]byte, error) { - type wrapper struct { - Description string `json:"description,omitempty"` - Oauth2MetadataURL string `json:"oauth2MetadataUrl,omitempty"` - Flows map[OAuthFlowName]any `json:"flows,omitempty"` - } - wrapped := wrapper{Description: s.Description, Oauth2MetadataURL: s.Oauth2MetadataURL} + wrapped := oauth2{Description: s.Description, Oauth2MetadataURL: s.Oauth2MetadataURL} switch v := s.Flows.(type) { case AuthorizationCodeOAuthFlow: - wrapped.Flows = map[OAuthFlowName]any{"authorizationCode": v} + wrapped.Flows = oauthFlows{AuthorizationCode: &v} case ClientCredentialsOAuthFlow: - wrapped.Flows = map[OAuthFlowName]any{"clientCredentials": v} + wrapped.Flows = oauthFlows{ClientCredentials: &v} case ImplicitOAuthFlow: - wrapped.Flows = map[OAuthFlowName]any{"implicit": v} + wrapped.Flows = oauthFlows{Implicit: &v} case PasswordOAuthFlow: - wrapped.Flows = map[OAuthFlowName]any{"password": v} + wrapped.Flows = oauthFlows{Password: &v} case DeviceCodeOAuthFlow: - wrapped.Flows = map[OAuthFlowName]any{"deviceCode": v} + wrapped.Flows = oauthFlows{DeviceCode: &v} default: return nil, fmt.Errorf("unknown OAuth flow type %T", v) } @@ -295,62 +298,45 @@ func (s OAuth2SecurityScheme) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler. func (s *OAuth2SecurityScheme) UnmarshalJSON(b []byte) error { - type wrapper struct { - Description string `json:"description,omitempty"` - Oauth2MetadataURL string `json:"oauth2MetadataUrl,omitempty"` - Flows map[OAuthFlowName]json.RawMessage `json:"flows,omitempty"` - } - var scheme wrapper + var scheme oauth2 if err := json.Unmarshal(b, &scheme); err != nil { return err } - - if len(scheme.Flows) != 1 { - return fmt.Errorf("expected exactly one OAuth flow, got %d", len(scheme.Flows)) + s.Description = scheme.Description + s.Oauth2MetadataURL = scheme.Oauth2MetadataURL + var n int + if scheme.Flows.AuthorizationCode != nil { + s.Flows = *scheme.Flows.AuthorizationCode + n++ } - - for name, rawMessage := range scheme.Flows { - switch name { - case "authorizationCode": - var flow AuthorizationCodeOAuthFlow - if err := json.Unmarshal(rawMessage, &flow); err != nil { - return err - } - s.Flows = flow - case "clientCredentials": - var flow ClientCredentialsOAuthFlow - if err := json.Unmarshal(rawMessage, &flow); err != nil { - return err - } - s.Flows = flow - case "implicit": - var flow ImplicitOAuthFlow - if err := json.Unmarshal(rawMessage, &flow); err != nil { - return err - } - s.Flows = flow - case "password": - var flow PasswordOAuthFlow - if err := json.Unmarshal(rawMessage, &flow); err != nil { - return err - } - s.Flows = flow - case "deviceCode": - var flow DeviceCodeOAuthFlow - if err := json.Unmarshal(rawMessage, &flow); err != nil { - return err - } - s.Flows = flow - default: - keys := make([]string, 0, len(scheme.Flows)) - for k := range scheme.Flows { - keys = append(keys, string(k)) - } - return fmt.Errorf("unknown OAuth flow type: %s, available: %v", name, keys) + if scheme.Flows.ClientCredentials != nil { + s.Flows = *scheme.Flows.ClientCredentials + n++ + } + if scheme.Flows.Implicit != nil { + s.Flows = *scheme.Flows.Implicit + n++ + } + if scheme.Flows.Password != nil { + s.Flows = *scheme.Flows.Password + n++ + } + if scheme.Flows.DeviceCode != nil { + s.Flows = *scheme.Flows.DeviceCode + n++ + } + if n == 0 { + var raw struct { + Flows json.RawMessage `json:"flows"` + } + if err := json.Unmarshal(b, &raw); err != nil { + return fmt.Errorf("unknown OAuth flow") } + return fmt.Errorf("unknown OAuth flow type: %v", jsonKeys(raw.Flows)) + } + if n != 1 { + return fmt.Errorf("expected exactly one OAuth flow, got %d", n) } - s.Description = scheme.Description - s.Oauth2MetadataURL = scheme.Oauth2MetadataURL return nil } diff --git a/a2a/core.go b/a2a/core.go index ef456fc9..a94a619d 100644 --- a/a2a/core.go +++ b/a2a/core.go @@ -15,7 +15,6 @@ package a2a import ( - "encoding/base64" "encoding/gob" "encoding/json" "fmt" @@ -90,57 +89,59 @@ type StreamResponse struct { Event } +type event struct { + Message *Message `json:"message,omitempty"` + Task *Task `json:"task,omitempty"` + StatusUpdate *TaskStatusUpdateEvent `json:"statusUpdate,omitempty"` + ArtifactUpdate *TaskArtifactUpdateEvent `json:"artifactUpdate,omitempty"` +} + // MarshalJSON implements json.Marshaler. func (sr StreamResponse) MarshalJSON() ([]byte, error) { - m := make(map[string]any) + wrapper := &event{} switch v := sr.Event.(type) { case *Message: - m["message"] = v + wrapper.Message = v case *Task: - m["task"] = v + wrapper.Task = v case *TaskStatusUpdateEvent: - m["statusUpdate"] = v + wrapper.StatusUpdate = v case *TaskArtifactUpdateEvent: - m["artifactUpdate"] = v + wrapper.ArtifactUpdate = v default: return nil, fmt.Errorf("unknown event type: %T", v) } - return json.Marshal(m) + return json.Marshal(wrapper) } // UnmarshalJSON implements json.Unmarshaler. func (sr *StreamResponse) UnmarshalJSON(data []byte) error { - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { + var wrapper event + if err := json.Unmarshal(data, &wrapper); err != nil { return fmt.Errorf("failed to unmarshal event: %w", err) } - - if v, ok := raw["message"]; ok { - var msg Message - if err := json.Unmarshal(v, &msg); err != nil { - return fmt.Errorf("failed to unmarshal Message event: %w", err) - } - sr.Event = &msg - } else if v, ok := raw["task"]; ok { - var task Task - if err := json.Unmarshal(v, &task); err != nil { - return fmt.Errorf("failed to unmarshal Task event: %w", err) - } - sr.Event = &task - } else if v, ok := raw["statusUpdate"]; ok { - var statusUpdate TaskStatusUpdateEvent - if err := json.Unmarshal(v, &statusUpdate); err != nil { - return fmt.Errorf("failed to unmarshal TaskStatusUpdateEvent: %w", err) - } - sr.Event = &statusUpdate - } else if v, ok := raw["artifactUpdate"]; ok { - var artifactUpdate TaskArtifactUpdateEvent - if err := json.Unmarshal(v, &artifactUpdate); err != nil { - return fmt.Errorf("failed to unmarshal TaskArtifactUpdateEvent: %w", err) - } - sr.Event = &artifactUpdate - } else { - return fmt.Errorf("unknown event type: %v", raw) + var n int + if wrapper.Message != nil { + sr.Event = wrapper.Message + n++ + } + if wrapper.Task != nil { + sr.Event = wrapper.Task + n++ + } + if wrapper.StatusUpdate != nil { + sr.Event = wrapper.StatusUpdate + n++ + } + if wrapper.ArtifactUpdate != nil { + sr.Event = wrapper.ArtifactUpdate + n++ + } + if n == 0 { + return fmt.Errorf("unknown event type: %v", jsonKeys(data)) + } + if n != 1 { + return fmt.Errorf("expected exactly one event type, got %d", n) } return nil } @@ -676,71 +677,78 @@ type Data struct { Value any } +type part struct { + Text *Text `json:"text,omitempty"` + Raw *Raw `json:"raw,omitempty"` + Data *any `json:"data,omitempty"` + URL *URL `json:"url,omitempty"` + Filename string `json:"filename,omitempty"` + MediaType string `json:"mediaType,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + // MarshalJSON custom serializer that flattens Content into the Part object. func (p Part) MarshalJSON() ([]byte, error) { - m := make(map[string]any) - + wrapper := &part{ + Filename: p.Filename, + MediaType: p.MediaType, + Metadata: p.Metadata, + } switch v := p.Content.(type) { case Text: - m["text"] = string(v) + wrapper.Text = &v case Raw: - m["raw"] = []byte(v) + wrapper.Raw = &v case Data: - m["data"] = v.Value + wrapper.Data = &v.Value case URL: - m["url"] = string(v) - } - - if p.Filename != "" { - m["filename"] = p.Filename - } - if p.MediaType != "" { - m["mediaType"] = p.MediaType - } - - if len(p.Metadata) > 0 { - m["metadata"] = p.Metadata + if v != "" { + wrapper.URL = &v + } } - return json.Marshal(m) + return json.Marshal(wrapper) } // UnmarshalJSON custom deserializer that hydrates Content from flattened fields. func (p *Part) UnmarshalJSON(b []byte) error { - var raw map[string]any - if err := json.Unmarshal(b, &raw); err != nil { + var wrapper part + if err := json.Unmarshal(b, &wrapper); err != nil { return err } - if v, ok := raw["text"].(string); ok { - p.Content = Text(v) - delete(raw, "text") - } else if v, ok := raw["raw"].(string); ok { - b, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return err - } - p.Content = Raw(b) - delete(raw, "raw") - } else if v, ok := raw["data"]; ok { - p.Content = Data{Value: v} - delete(raw, "data") - } else if v, ok := raw["url"].(string); ok { - p.Content = URL(v) - delete(raw, "url") - } + p.Filename = wrapper.Filename + p.MediaType = wrapper.MediaType + p.Metadata = wrapper.Metadata - if filename, ok := raw["filename"].(string); ok { - p.Filename = filename - delete(raw, "filename") + var content PartContent + var n int + if wrapper.Text != nil { + content = *wrapper.Text + n++ + } + if wrapper.Raw != nil { + content = *wrapper.Raw + n++ + } + if wrapper.Data != nil { + content = Data{Value: *wrapper.Data} + n++ } - if mediaType, ok := raw["mediaType"].(string); ok { - p.MediaType = mediaType - delete(raw, "mediaType") + if wrapper.URL != nil && *wrapper.URL != "" { + content = *wrapper.URL + n++ + } + + if n == 0 { + return fmt.Errorf("unknown part content type: %v", jsonKeys(b)) } - if m, ok := raw["metadata"].(map[string]any); ok { - p.Metadata = m + if n > 1 { + return fmt.Errorf("expected exactly one of text, raw, data, or url, got %d", n) } + + p.Content = content + return nil } @@ -899,3 +907,15 @@ type SubscribeToTaskRequest struct { // ID is the ID of the task to subscribe to. ID TaskID `json:"id" yaml:"id" mapstructure:"id"` } + +func jsonKeys(data []byte) []string { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return nil + } + keys := make([]string, 0, len(raw)) + for k := range raw { + keys = append(keys, k) + } + return keys +} diff --git a/a2a/event_json_test.go b/a2a/event_json_test.go index e890a126..e80d3ca2 100644 --- a/a2a/event_json_test.go +++ b/a2a/event_json_test.go @@ -120,7 +120,7 @@ func TestEventMarshalJSON(t *testing.T) { } } -// TestUnmarshalEventJSON tests that UnmarshalEventJSON correctly discriminates based on 'kind'. +// TestUnmarshalEventJSON tests that "oneof" convention JSON is unmarshaled into the correct Event types. func TestUnmarshalEventJSON(t *testing.T) { testCases := []struct { name string @@ -130,7 +130,7 @@ func TestUnmarshalEventJSON(t *testing.T) { }{ { name: "Message", - json: `{"message":{"messageId":"msg-123","role":"ROLE_USER","parts":[{"kind":"text","text":"hello"}]}}`, + json: `{"message":{"messageId":"msg-123","role":"ROLE_USER","parts":[{"text":"hello"}]}}`, wantType: "*a2a.Message", checkFunc: func(t *testing.T, event Event) { msg, ok := event.(*Message) @@ -181,7 +181,7 @@ func TestUnmarshalEventJSON(t *testing.T) { }, { name: "TaskArtifactUpdateEvent", - json: `{"artifactUpdate":{"taskId":"task-123","contextId":"ctx-123","artifact":{"artifactId":"art-123","parts":[{"kind":"text","text":"result"}]}}}`, + json: `{"artifactUpdate":{"taskId":"task-123","contextId":"ctx-123","artifact":{"artifactId":"art-123","parts":[{"text":"result"}]}}}`, wantType: "*a2a.TaskArtifactUpdateEvent", checkFunc: func(t *testing.T, event Event) { artifactUpdate, ok := event.(*TaskArtifactUpdateEvent) @@ -230,12 +230,17 @@ func TestUnmarshalEventJSON_Errors(t *testing.T) { { name: "unknown type", json: `{"unknown": {"id":"123"}}`, - wantErr: "unknown event type:", + wantErr: "unknown event type: [unknown]", }, { name: "malformed task", json: `{"task":{"id":123}}`, - wantErr: "failed to unmarshal Task event", + wantErr: "failed to unmarshal event", + }, + { + name: "more than one event type", + json: `{"task":{"id":"123"}, "message":{"id":"123"}}`, + wantErr: "expected exactly one event type, got 2", }, } diff --git a/a2a/json_test.go b/a2a/json_test.go index 344fe8fa..2176d006 100644 --- a/a2a/json_test.go +++ b/a2a/json_test.go @@ -52,20 +52,32 @@ func TestContentPartsJSONCodec(t *testing.T) { jsons := []string{ `{"text":"hello, world"}`, `{"data":{"foo":"bar"}}`, - `{"filename":"foo","url":"https://cats.com/1.png"}`, - `{"filename":"foo","mediaType":"image/png","raw":"//4="}`, - `{"metadata":{"foo":"bar"},"text":"42"}`, + `{"url":"https://cats.com/1.png","filename":"foo"}`, + `{"raw":"//4=","filename":"foo","mediaType":"image/png"}`, + `{"text":"42","metadata":{"foo":"bar"}}`, } wantJSON := fmt.Sprintf("[%s]", strings.Join(jsons, ",")) if got := mustMarshal(t, parts); got != wantJSON { - t.Fatalf("Marshal() failed:\nwant %v\ngot: %s", wantJSON, got) + t.Fatalf("Marshal() failed:\ngot: %s \nwant: %s", got, wantJSON) } var got ContentParts mustUnmarshal(t, []byte(wantJSON), &got) if !reflect.DeepEqual(got, parts) { - t.Fatalf("Unmarshal() failed:\nwant %#v\ngot: %#v", parts, got) + t.Fatalf("Unmarshal() failed:\ngot: %#v \nwant: %#v", got, parts) + } +} + +func TestContentPartsJSONCodecUnknownType(t *testing.T) { + wrongJSON := `[{"unknown":"hello, world"}]` + var gotWrong ContentParts + err := json.Unmarshal([]byte(wrongJSON), &gotWrong) + if err == nil { + t.Fatalf("Unmarshal() should have failed with unknown part content type") + } + if err.Error() != "unknown part content type: [unknown]" { + t.Fatalf("got: %v, want: %v", err, "unknown part content type: [unknown]") } } @@ -97,14 +109,45 @@ func TestSecuritySchemeJSONCodec(t *testing.T) { var decodedJSON NamedSecuritySchemes mustUnmarshal(t, []byte(wantJSON), &decodedJSON) if !reflect.DeepEqual(decodedJSON, schemes) { - t.Fatalf("Unmarshal() failed:\nwant %+v\ngot: %s", schemes, decodedJSON) + t.Fatalf("Unmarshal() failed:\ngot: %s \nwant: %s", decodedJSON, schemes) } encodedSchemes := mustMarshal(t, &schemes) var decodedBack NamedSecuritySchemes mustUnmarshal(t, []byte(encodedSchemes), &decodedBack) if !reflect.DeepEqual(decodedJSON, decodedBack) { - t.Fatalf("Decoding back failed:\nwant %+v\ngot: %s", decodedJSON, decodedBack) + t.Fatalf("Decoding back failed:\ngot: %s \nwant: %s", decodedBack, decodedJSON) + } +} + +func TestSecuritySchemeJSONUnmarshalUnknownType(t *testing.T) { + tests := []struct { + name string + json string + wantError string + }{ + { + name: "unknown security scheme type", + json: `{"name1":{"unknown":"abc"}}`, + wantError: "unknown security scheme type for name1: [unknown]", + }, + { + name: "unknown oauth flow type", + json: `{"name2":{"oauth2SecurityScheme":{"oauth2MetadataUrl": "https://test.com", "description": "test","flows":{"unknown":{"scopes":{"email":"read user emails"},"tokenUrl":"url"}}}}}`, + wantError: "unknown OAuth flow type: [unknown]", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var gotWrong NamedSecuritySchemes + err := json.Unmarshal([]byte(tc.json), &gotWrong) + if err == nil { + t.Fatalf("Unmarshal() should have failed with %s", tc.name) + } + if err.Error() != tc.wantError { + t.Fatalf("got: %v, want: %v", err, tc.wantError) + } + }) } } @@ -261,7 +304,7 @@ func TestAgentCardParsing(t *testing.T) { } if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("AgentCard codec diff(-want +got):\n%v", diff) + t.Errorf("AgentCard codec diff(+got -want):\n%v", diff) } }