1package provider
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "reflect"
9 "strings"
10 "testing"
11)
12
13func TestParseCopilotAvailableModels(t *testing.T) {
14 for _, tc := range []struct {
15 name, body string
16 fallback bool
17 want []string
18 invalid bool
19 }{
20 {"picker", `{"data":[{"id":"yes","model_picker_enabled":true},{"id":"disabled","model_picker_enabled":true,"policy":{"state":"disabled"}},{"id":"hidden","policy":{"state":"enabled"}},{"id":"no-tools","model_picker_enabled":true,"capabilities":{"supports":{"tool_calls":false}}}]}`, true, []string{"yes"}, false},
21 {"individual fallback", `{"data":[{"id":"enabled","policy":{"state":"enabled"}},{"id":"unconfigured","policy":{"state":"unconfigured"}},{"id":"disabled","policy":{"state":"disabled"}}]}`, true, []string{"enabled"}, false},
22 {"enterprise no fallback", `{"data":[{"id":"enabled","policy":{"state":"enabled"}}]}`, false, []string{}, false},
23 {"empty", `{"data":[]}`, true, []string{}, false},
24 {"duplicate", `{"data":[{"id":"yes","model_picker_enabled":true},{"id":"yes","model_picker_enabled":true}]}`, false, []string{"yes"}, false},
25 {"missing", `{}`, false, nil, true},
26 {"null", `{"data":null}`, false, nil, true},
27 {"invalid", `{"data":{}}`, false, nil, true},
28 } {
29 t.Run(tc.name, func(t *testing.T) {
30 got, err := parseCopilotAvailableModels([]byte(tc.body), tc.fallback)
31 if (err != nil) != tc.invalid {
32 t.Fatalf("error = %v", err)
33 }
34 if !tc.invalid && !reflect.DeepEqual(got, tc.want) {
35 t.Fatalf("got %v, want %v", got, tc.want)
36 }
37 })
38 }
39}
40
41func TestCopilotDiscoveryHTTP(t *testing.T) {
42 for _, status := range []int{200, 403, 429} {
43 t.Run(fmt.Sprint(status), func(t *testing.T) {
44 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45 if r.Method != "GET" || r.URL.Path != "/models" {
46 t.Error("incorrect discovery request")
47 }
48 if r.Header.Get("Authorization") != "Bearer synthetic-session-token" || r.Header.Get("Copilot-Integration-Id") != "vscode-chat" || r.Header.Get("X-GitHub-Api-Version") != "2026-06-01" {
49 t.Error("incorrect discovery headers")
50 }
51 w.WriteHeader(status)
52 if status != 200 {
53 fmt.Fprint(w, "sensitive-response-body")
54 return
55 }
56 fmt.Fprint(w, `{"data":[{"id":"claude-sonnet-5","model_picker_enabled":true}]}`)
57 }))
58 defer srv.Close()
59 pat := seedCopilotTestToken(t, srv.URL)
60 got, err := DiscoverCopilotAvailableModels(context.Background(), pat)
61 if status == 200 {
62 if err != nil || !reflect.DeepEqual(got, []string{"claude-sonnet-5"}) {
63 t.Fatalf("got %v, %v", got, err)
64 }
65 } else if err == nil || strings.Contains(err.Error(), "sensitive-response-body") {
66 t.Fatalf("unexpected error %v", err)
67 }
68 ctx, cancel := context.WithCancel(context.Background())
69 cancel()
70 if _, err := DiscoverCopilotAvailableModels(ctx, pat); err == nil {
71 t.Error("cancellation ignored")
72 }
73 })
74 }
75}
76
77func TestModelAvailabilityFiltersWithoutDeletingCatalog(t *testing.T) {
78 t.Cleanup(func() { SetModelAvailability("github-copilot", nil) })
79 before := len(ModelsForProvider("anthropic"))
80 ids := []string{"claude-sonnet-5"}
81 SetModelAvailability("github-copilot", ids)
82 ids[0] = "mutated"
83 models := ModelsForProvider("github-copilot")
84 if len(models) != 1 || models[0].ID != "claude-sonnet-5" {
85 t.Fatalf("models = %v", models)
86 }
87 if len(ModelsForProvider("anthropic")) != before {
88 t.Error("changed another provider")
89 }
90 SetModelAvailability("github-copilot", []string{})
91 // Hiding a model must not change wire metadata for a session already using it.
92 model, err := FindModel("github-copilot", "gpt-5.4")
93 if err != nil || model.API != APIResponses {
94 t.Fatalf("availability refresh lost routing metadata: %+v, %v", model, err)
95 }
96 if len(ModelsForProvider("github-copilot")) != 0 {
97 t.Error("empty availability did not hide models")
98 }
99 SetModelAvailability("github-copilot", nil)
100 if len(ModelsForProvider("github-copilot")) != 38 {
101 t.Error("clearing restriction did not restore catalog")
102 }
103}