Skip to content

Commit bc93c27

Browse files
committed
Expose catalogue search bindings
1 parent c1a7d5b commit bc93c27

77 files changed

Lines changed: 5507 additions & 797 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/update-copilot-dependency.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ jobs:
7373
run: npm install
7474

7575
- name: Install codegen dependencies
76+
env:
77+
VERSION: ${{ inputs.version }}
7678
working-directory: ./scripts/codegen
77-
run: npm ci
79+
run: npm install --save-dev --save-exact "@github/copilot@$VERSION"
7880

7981
- name: Run codegen
8082
working-directory: ./scripts/codegen
@@ -95,7 +97,7 @@ jobs:
9597
env:
9698
VERSION: ${{ inputs.version }}
9799
working-directory: ./java/scripts/codegen
98-
run: npm install "@github/copilot@$VERSION"
100+
run: npm install --save-exact "@github/copilot@$VERSION"
99101

100102
- name: Update Java POM CLI version property
101103
env:

dotnet/src/Generated/Rpc.cs

Lines changed: 485 additions & 152 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Generated/SessionEvents.cs

Lines changed: 570 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization.Metadata;
3+
using GitHub.Copilot.Rpc;
4+
using Xunit;
5+
6+
#pragma warning disable GHCP001 // The catalogue search schema is experimental in CLI 1.0.83-2.
7+
8+
namespace GitHub.Copilot.Test.Unit;
9+
10+
public class CatalogueConformanceTests
11+
{
12+
private const string OpaqueMcpHandle = "opaque:mcp/01-do-not-parse";
13+
private const string OpaqueSkillHandle = "opaque:skill/02-do-not-parse";
14+
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
15+
{
16+
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
17+
};
18+
19+
[Fact]
20+
public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles()
21+
{
22+
const string json = """
23+
{
24+
"kind": "succeeded",
25+
"searchId": "search-01",
26+
"candidates": [
27+
{
28+
"kind": "mcp-server",
29+
"handle": "opaque:mcp/01-do-not-parse",
30+
"handleExpiresAt": "2026-09-02T12:00:00Z",
31+
"mediaType": "application/mcp-server-card+json",
32+
"installability": "installable",
33+
"displayName": "Example MCP",
34+
"rawCard": { "secret": "must-not-survive" },
35+
"source": { "kind": "url", "url": "https://catalog.example/mcp.json" },
36+
"provenance": {
37+
"authority": "catalog.example",
38+
"observedAt": "2026-09-02T11:00:00Z",
39+
"mediaType": "application/mcp-server-card+json"
40+
}
41+
},
42+
{
43+
"kind": "ai-skill",
44+
"handle": "opaque:skill/02-do-not-parse",
45+
"handleExpiresAt": "2026-09-02T12:00:00Z",
46+
"mediaType": "application/ai-skill",
47+
"installability": "not-installable-kind",
48+
"displayName": "Example skill",
49+
"rawCard": { "secret": "must-not-survive" },
50+
"source": { "kind": "embedded" },
51+
"provenance": {
52+
"authority": "catalog.example",
53+
"observedAt": "2026-09-02T11:00:00Z",
54+
"mediaType": "application/ai-skill"
55+
}
56+
}
57+
],
58+
"truncated": false,
59+
"negotiated": {
60+
"runtimeProtocolVersion": 1,
61+
"grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"]
62+
}
63+
}
64+
""";
65+
66+
var result = Assert.IsType<CatalogSearchResultSucceeded>(
67+
JsonSerializer.Deserialize<CatalogSearchResult>(json, SerializerOptions));
68+
var mcp = Assert.IsType<CatalogCandidateMcpServer>(result.Candidates[0]);
69+
var skill = Assert.IsType<CatalogCandidateAiSkill>(result.Candidates[1]);
70+
Assert.Equal(OpaqueMcpHandle, mcp.Handle);
71+
Assert.Equal(OpaqueSkillHandle, skill.Handle);
72+
Assert.IsType<CatalogCandidateSourceUrl>(mcp.Source);
73+
Assert.IsType<CatalogCandidateSourceEmbedded>(skill.Source);
74+
75+
using var encoded = JsonDocument.Parse(JsonSerializer.Serialize<CatalogSearchResult>(
76+
result, SerializerOptions));
77+
foreach (var candidate in encoded.RootElement.GetProperty("candidates").EnumerateArray())
78+
{
79+
Assert.False(candidate.TryGetProperty("card", out _));
80+
Assert.False(candidate.TryGetProperty("cardData", out _));
81+
Assert.False(candidate.TryGetProperty("rawCard", out _));
82+
}
83+
}
84+
85+
[Fact]
86+
public void CatalogSearchResult_PreservesRefusalsAndFailures()
87+
{
88+
var authentication = JsonSerializer.Deserialize<CatalogSearchResult>(
89+
"""{"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."}""",
90+
SerializerOptions);
91+
Assert.IsType<CatalogSearchResultAuthenticationRequired>(authentication);
92+
93+
var network = Assert.IsType<CatalogSearchResultNetworkFailure>(
94+
JsonSerializer.Deserialize<CatalogSearchResult>(
95+
"""{"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."}""",
96+
SerializerOptions));
97+
Assert.Equal(30, network.RetryAfterSeconds);
98+
}
99+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package rpc
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
const (
9+
opaqueMCPHandle = "opaque:mcp/01-do-not-parse"
10+
opaqueSkillHandle = "opaque:skill/02-do-not-parse"
11+
)
12+
13+
func TestCatalogSearchResultPreservesCandidateSemantics(t *testing.T) {
14+
result, err := unmarshalCatalogSearchResult([]byte(`{
15+
"kind":"succeeded",
16+
"searchId":"search-01",
17+
"candidates":[
18+
{
19+
"kind":"mcp-server",
20+
"handle":"opaque:mcp/01-do-not-parse",
21+
"handleExpiresAt":"2026-09-02T12:00:00Z",
22+
"mediaType":"application/mcp-server-card+json",
23+
"installability":"installable",
24+
"displayName":"Example MCP",
25+
"rawCard":{"secret":"must-not-survive"},
26+
"source":{"kind":"url","url":"https://catalog.example/mcp.json"},
27+
"provenance":{
28+
"authority":"catalog.example",
29+
"observedAt":"2026-09-02T11:00:00Z",
30+
"mediaType":"application/mcp-server-card+json"
31+
}
32+
},
33+
{
34+
"kind":"ai-skill",
35+
"handle":"opaque:skill/02-do-not-parse",
36+
"handleExpiresAt":"2026-09-02T12:00:00Z",
37+
"mediaType":"application/ai-skill",
38+
"installability":"not-installable-kind",
39+
"displayName":"Example skill",
40+
"rawCard":{"secret":"must-not-survive"},
41+
"source":{"kind":"embedded"},
42+
"provenance":{
43+
"authority":"catalog.example",
44+
"observedAt":"2026-09-02T11:00:00Z",
45+
"mediaType":"application/ai-skill"
46+
}
47+
}
48+
],
49+
"truncated":false,
50+
"negotiated":{
51+
"runtimeProtocolVersion":1,
52+
"grantedCapabilities":["mcp-server-card","ai-skill-discovery"]
53+
}
54+
}`))
55+
if err != nil {
56+
t.Fatalf("unmarshal catalogue success: %v", err)
57+
}
58+
59+
success, ok := result.(*CatalogSearchSucceeded)
60+
if !ok {
61+
t.Fatalf("catalogue result = %T, want *CatalogSearchSucceeded", result)
62+
}
63+
mcp, ok := success.Candidates[0].(*CatalogMCPServerCandidate)
64+
if !ok {
65+
t.Fatalf("first candidate = %T, want *CatalogMCPServerCandidate", success.Candidates[0])
66+
}
67+
skill, ok := success.Candidates[1].(*CatalogAiSkillCandidate)
68+
if !ok {
69+
t.Fatalf("second candidate = %T, want *CatalogAiSkillCandidate", success.Candidates[1])
70+
}
71+
if mcp.Handle != opaqueMCPHandle || skill.Handle != opaqueSkillHandle {
72+
t.Fatalf("opaque handles changed: %q, %q", mcp.Handle, skill.Handle)
73+
}
74+
if _, ok := mcp.Source.(*CatalogCandidateSourceURL); !ok {
75+
t.Fatalf("MCP source = %T, want *CatalogCandidateSourceURL", mcp.Source)
76+
}
77+
if _, ok := skill.Source.(*CatalogCandidateSourceEmbedded); !ok {
78+
t.Fatalf("skill source = %T, want *CatalogCandidateSourceEmbedded", skill.Source)
79+
}
80+
81+
encoded, err := json.Marshal(success)
82+
if err != nil {
83+
t.Fatalf("marshal catalogue success: %v", err)
84+
}
85+
var wire map[string]any
86+
if err := json.Unmarshal(encoded, &wire); err != nil {
87+
t.Fatalf("decode catalogue wire result: %v", err)
88+
}
89+
for _, candidate := range wire["candidates"].([]any) {
90+
fields := candidate.(map[string]any)
91+
for _, forbidden := range []string{"card", "cardData", "rawCard"} {
92+
if _, exists := fields[forbidden]; exists {
93+
t.Fatalf("candidate leaked %q: %s", forbidden, encoded)
94+
}
95+
}
96+
}
97+
}
98+
99+
func TestCatalogSearchResultPreservesRefusalsAndFailures(t *testing.T) {
100+
tests := []struct {
101+
name string
102+
payload string
103+
assert func(*testing.T, CatalogSearchResult)
104+
}{
105+
{
106+
name: "authentication required",
107+
payload: `{"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."}`,
108+
assert: func(t *testing.T, result CatalogSearchResult) {
109+
if _, ok := result.(*CatalogAuthenticationRequiredError); !ok {
110+
t.Fatalf("result = %T, want *CatalogAuthenticationRequiredError", result)
111+
}
112+
},
113+
},
114+
{
115+
name: "network failure",
116+
payload: `{"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."}`,
117+
assert: func(t *testing.T, result CatalogSearchResult) {
118+
failure, ok := result.(*CatalogNetworkFailureError)
119+
if !ok {
120+
t.Fatalf("result = %T, want *CatalogNetworkFailureError", result)
121+
}
122+
if failure.RetryAfterSeconds == nil || *failure.RetryAfterSeconds != 30 {
123+
t.Fatalf("retryAfterSeconds = %v, want 30", failure.RetryAfterSeconds)
124+
}
125+
},
126+
},
127+
}
128+
129+
for _, test := range tests {
130+
t.Run(test.name, func(t *testing.T) {
131+
result, err := unmarshalCatalogSearchResult([]byte(test.payload))
132+
if err != nil {
133+
t.Fatalf("unmarshal catalogue result: %v", err)
134+
}
135+
test.assert(t, result)
136+
})
137+
}
138+
}

0 commit comments

Comments
 (0)