@@ -10,9 +10,10 @@ const (
1010 opaqueSkillHandle = "opaque:skill/02-do-not-parse"
1111)
1212
13- func TestCatalogSearchResultPreservesCandidateSemantics (t * testing.T ) {
13+ func TestClosedDiscriminatedUnionPreservesKnownNestedVariants (t * testing.T ) {
1414 result , err := unmarshalCatalogSearchResult ([]byte (`{
1515 "kind":"succeeded",
16+ "rawCard":{"secret":"must-not-survive"},
1617 "searchId":"search-01",
1718 "candidates":[
1819 {
@@ -23,7 +24,7 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) {
2324 "installability":"installable",
2425 "displayName":"Example MCP",
2526 "rawCard":{"secret":"must-not-survive"},
26- "source":{"kind":"url","url":"https://catalog.example/mcp.json"},
27+ "source":{"kind":"url","url":"https://catalog.example/mcp.json","rawCard":{"secret":"must-not-survive"} },
2728 "provenance":{
2829 "authority":"catalog.example",
2930 "observedAt":"2026-09-02T11:00:00Z",
@@ -38,7 +39,7 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) {
3839 "installability":"not-installable-kind",
3940 "displayName":"Example skill",
4041 "rawCard":{"secret":"must-not-survive"},
41- "source":{"kind":"embedded"},
42+ "source":{"kind":"embedded","rawCard":{"secret":"must-not-survive"} },
4243 "provenance":{
4344 "authority":"catalog.example",
4445 "observedAt":"2026-09-02T11:00:00Z",
@@ -86,17 +87,23 @@ func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) {
8687 if err := json .Unmarshal (encoded , & wire ); err != nil {
8788 t .Fatalf ("decode catalogue wire result: %v" , err )
8889 }
90+ if _ , exists := wire ["rawCard" ]; exists {
91+ t .Fatalf ("result leaked rawCard: %s" , encoded )
92+ }
8993 for _ , candidate := range wire ["candidates" ].([]any ) {
9094 fields := candidate .(map [string ]any )
9195 for _ , forbidden := range []string {"card" , "cardData" , "rawCard" } {
9296 if _ , exists := fields [forbidden ]; exists {
9397 t .Fatalf ("candidate leaked %q: %s" , forbidden , encoded )
9498 }
9599 }
100+ if _ , exists := fields ["source" ].(map [string ]any )["rawCard" ]; exists {
101+ t .Fatalf ("candidate source leaked rawCard: %s" , encoded )
102+ }
96103 }
97104}
98105
99- func TestCatalogSearchResultPreservesRefusalsAndFailures (t * testing.T ) {
106+ func TestClosedDiscriminatedUnionPreservesRefusalsAndFailures (t * testing.T ) {
100107 tests := []struct {
101108 name string
102109 payload string
@@ -137,19 +144,38 @@ func TestCatalogSearchResultPreservesRefusalsAndFailures(t *testing.T) {
137144 }
138145}
139146
140- func TestCatalogSearchResultRejectsUnknownCandidateKinds (t * testing.T ) {
141- _ , err := unmarshalCatalogSearchResult ([]byte (`{
142- "kind":"succeeded",
143- "searchId":"search-unknown",
144- "candidates":[{
145- "kind":"future-kind",
146- "handle":"opaque:future/03-do-not-parse",
147- "rawCard":{"secret":"must-not-survive"}
148- }],
149- "truncated":false,
150- "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}
151- }` ))
152- if err == nil {
153- t .Fatal ("unknown catalogue candidate kind with rawCard must be rejected" )
147+ func TestClosedDiscriminatedUnionRejectsUnknownAndMissingDiscriminators (t * testing.T ) {
148+ validCandidatePrefix := `{
149+ "handle":"opaque:mcp/01-do-not-parse",
150+ "handleExpiresAt":"2026-09-02T12:00:00Z",
151+ "mediaType":"application/mcp-server-card+json",
152+ "installability":"installable",
153+ "displayName":"Example MCP",
154+ "provenance":{
155+ "authority":"catalog.example",
156+ "observedAt":"2026-09-02T11:00:00Z",
157+ "mediaType":"application/mcp-server-card+json"
158+ },`
159+ searchPrefix := `{"kind":"succeeded","searchId":"search-invalid","candidates":[`
160+ searchSuffix := `],"truncated":false,"negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}}`
161+ tests := map [string ]string {
162+ "unknown outer discriminator" : `{"kind":"future-result","rawCard":{"secret":"must-not-survive"}}` ,
163+ "missing outer discriminator" : `{"rawCard":{"secret":"must-not-survive"}}` ,
164+ "unknown candidate discriminator" : searchPrefix + validCandidatePrefix +
165+ `"kind":"future-candidate","source":{"kind":"url","url":"https://catalog.example/mcp.json"},"rawCard":{"secret":"must-not-survive"}}` + searchSuffix ,
166+ "missing candidate discriminator" : searchPrefix + validCandidatePrefix +
167+ `"source":{"kind":"url","url":"https://catalog.example/mcp.json"},"rawCard":{"secret":"must-not-survive"}}` + searchSuffix ,
168+ "unknown nested discriminator" : searchPrefix + validCandidatePrefix +
169+ `"kind":"mcp-server","source":{"kind":"future-source","rawCard":{"secret":"must-not-survive"}}}` + searchSuffix ,
170+ "missing nested discriminator" : searchPrefix + validCandidatePrefix +
171+ `"kind":"mcp-server","source":{"rawCard":{"secret":"must-not-survive"}}}` + searchSuffix ,
172+ }
173+
174+ for name , payload := range tests {
175+ t .Run (name , func (t * testing.T ) {
176+ if _ , err := unmarshalCatalogSearchResult ([]byte (payload )); err == nil {
177+ t .Fatal ("invalid closed union payload must be rejected" )
178+ }
179+ })
154180 }
155181}
0 commit comments