208 lines
7.2 KiB
Go
208 lines
7.2 KiB
Go
package registry
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestEmbeddedCodexClientModelsCatalogIsValid(t *testing.T) {
|
|
data, revision := GetCodexClientModelsSnapshot()
|
|
if revision == 0 {
|
|
t.Fatal("embedded Codex client model catalog revision = 0, want non-zero")
|
|
}
|
|
if err := ValidateCodexClientModelsJSON(data); err != nil {
|
|
t.Fatalf("embedded Codex client model catalog is invalid: %v", err)
|
|
}
|
|
|
|
data[0] ^= 0xff
|
|
second, secondRevision := GetCodexClientModelsSnapshot()
|
|
if secondRevision != revision {
|
|
t.Fatalf("snapshot revision = %d, want %d", secondRevision, revision)
|
|
}
|
|
if err := ValidateCodexClientModelsJSON(second); err != nil {
|
|
t.Fatalf("mutating returned snapshot changed stored catalog: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateCodexClientModelsJSON(t *testing.T) {
|
|
validDefault := testCodexClientModel("gpt-5.5", 1)
|
|
validOther := testCodexClientModel("gpt-5.6-sol", 2)
|
|
emptySlug := testCodexClientModel("gpt-5.5", 1)
|
|
emptySlug["slug"] = ""
|
|
missingField := testCodexClientModel("gpt-5.5", 1)
|
|
delete(missingField, "base_instructions")
|
|
wrongFieldType := testCodexClientModel("gpt-5.5", 1)
|
|
wrongFieldType["context_window"] = "372000"
|
|
unsupportedDefault := testCodexClientModel("gpt-5.5", 1)
|
|
unsupportedDefault["default_reasoning_level"] = "high"
|
|
|
|
tests := []struct {
|
|
name string
|
|
raw []byte
|
|
}{
|
|
{name: "malformed", raw: []byte(`{"models":`)},
|
|
{name: "empty", raw: []byte(`{"models":[]}`)},
|
|
{name: "empty slug", raw: testCodexClientCatalog(t, emptySlug)},
|
|
{name: "duplicate slug", raw: testCodexClientCatalog(t, validDefault, validDefault)},
|
|
{name: "missing default", raw: testCodexClientCatalog(t, validOther)},
|
|
{name: "missing required field", raw: testCodexClientCatalog(t, missingField)},
|
|
{name: "wrong required field type", raw: testCodexClientCatalog(t, wrongFieldType)},
|
|
{name: "default reasoning level not supported", raw: testCodexClientCatalog(t, unsupportedDefault)},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if err := ValidateCodexClientModelsJSON(tt.raw); err == nil {
|
|
t.Fatal("ValidateCodexClientModelsJSON() error = nil, want error")
|
|
}
|
|
})
|
|
}
|
|
|
|
valid := testCodexClientCatalog(t, validDefault, validOther)
|
|
if err := ValidateCodexClientModelsJSON(valid); err != nil {
|
|
t.Fatalf("valid catalog rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadCodexClientModelsRejectsInvalidWithoutReplacing(t *testing.T) {
|
|
original, _ := GetCodexClientModelsSnapshot()
|
|
t.Cleanup(func() {
|
|
if _, err := loadCodexClientModelsFromBytes(original, "test cleanup"); err != nil {
|
|
t.Fatalf("restore original catalog: %v", err)
|
|
}
|
|
})
|
|
|
|
valid := testCodexClientCatalog(t, testCodexClientModel("gpt-5.5", 1))
|
|
changed, err := loadCodexClientModelsFromBytes(valid, "test")
|
|
if err != nil {
|
|
t.Fatalf("load valid catalog: %v", err)
|
|
}
|
|
if !changed {
|
|
t.Fatal("load valid catalog changed = false, want true")
|
|
}
|
|
beforeInvalid, revision := GetCodexClientModelsSnapshot()
|
|
|
|
if _, err = loadCodexClientModelsFromBytes([]byte(`{"models":[]}`), "test invalid"); err == nil {
|
|
t.Fatal("load invalid catalog error = nil, want error")
|
|
}
|
|
afterInvalid, afterRevision := GetCodexClientModelsSnapshot()
|
|
if string(afterInvalid) != string(beforeInvalid) {
|
|
t.Fatal("invalid catalog replaced current snapshot")
|
|
}
|
|
if afterRevision != revision {
|
|
t.Fatalf("revision after invalid catalog = %d, want %d", afterRevision, revision)
|
|
}
|
|
}
|
|
|
|
func TestFetchCodexClientModelsFallsBackToNextURL(t *testing.T) {
|
|
invalidServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"models":[{"slug":"gpt-5.6-sol"}]}`))
|
|
}))
|
|
defer invalidServer.Close()
|
|
|
|
validCatalog := testCodexClientCatalog(t, testCodexClientModel("gpt-5.5", 1))
|
|
validServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
t.Errorf("method = %s, want GET", r.Method)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write(validCatalog)
|
|
}))
|
|
defer validServer.Close()
|
|
|
|
previousURLs := codexClientModelsURLs
|
|
codexClientModelsURLs = []string{invalidServer.URL, validServer.URL}
|
|
t.Cleanup(func() { codexClientModelsURLs = previousURLs })
|
|
|
|
data, sourceURL := fetchCodexClientModelsFromRemote(context.Background())
|
|
if sourceURL != validServer.URL {
|
|
t.Fatalf("source URL = %q, want %q", sourceURL, validServer.URL)
|
|
}
|
|
if string(data) != string(validCatalog) {
|
|
t.Fatalf("catalog = %s, want %s", data, validCatalog)
|
|
}
|
|
}
|
|
|
|
func TestRefreshCodexClientModelsKeepsLastValidSnapshot(t *testing.T) {
|
|
original, _ := GetCodexClientModelsSnapshot()
|
|
previousURLs := codexClientModelsURLs
|
|
t.Cleanup(func() {
|
|
codexClientModelsURLs = previousURLs
|
|
if _, err := loadCodexClientModelsFromBytes(original, "test cleanup"); err != nil {
|
|
t.Fatalf("restore original catalog: %v", err)
|
|
}
|
|
})
|
|
|
|
lastValid := testCodexClientCatalog(t, testCodexClientModel("gpt-5.5", 1))
|
|
if _, err := loadCodexClientModelsFromBytes(lastValid, "test last valid"); err != nil {
|
|
t.Fatalf("load last valid catalog: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
statusCode int
|
|
body string
|
|
}{
|
|
{name: "remote files missing", statusCode: http.StatusNotFound},
|
|
{name: "remote JSON malformed", statusCode: http.StatusOK, body: `{"models":`},
|
|
{name: "remote JSON incomplete", statusCode: http.StatusOK, body: `{"models":[{"slug":"gpt-5.5"}]}`},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
servers := make([]*httptest.Server, 0, 2)
|
|
urls := make([]string, 0, 2)
|
|
for range 2 {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(tt.statusCode)
|
|
_, _ = w.Write([]byte(tt.body))
|
|
}))
|
|
servers = append(servers, server)
|
|
urls = append(urls, server.URL)
|
|
}
|
|
defer func() {
|
|
for _, server := range servers {
|
|
server.Close()
|
|
}
|
|
}()
|
|
|
|
before, revision := GetCodexClientModelsSnapshot()
|
|
codexClientModelsURLs = urls
|
|
tryRefreshCodexClientModels(context.Background(), "test refresh")
|
|
after, afterRevision := GetCodexClientModelsSnapshot()
|
|
if string(after) != string(before) {
|
|
t.Fatal("failed remote refresh replaced last valid catalog")
|
|
}
|
|
if afterRevision != revision {
|
|
t.Fatalf("revision after failed refresh = %d, want %d", afterRevision, revision)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testCodexClientModel(slug string, priority int) map[string]any {
|
|
return map[string]any{
|
|
"slug": slug,
|
|
"display_name": "Test " + slug,
|
|
"description": "Test model",
|
|
"base_instructions": "Test instructions",
|
|
"minimal_client_version": "0.144.0",
|
|
"visibility": "list",
|
|
"context_window": 372000,
|
|
"max_context_window": 372000,
|
|
"priority": priority,
|
|
"default_reasoning_level": "medium",
|
|
"supported_reasoning_levels": []map[string]any{{"effort": "medium", "description": "Balanced"}},
|
|
}
|
|
}
|
|
|
|
func testCodexClientCatalog(t *testing.T, models ...map[string]any) []byte {
|
|
t.Helper()
|
|
data, err := json.Marshal(map[string]any{"models": models})
|
|
if err != nil {
|
|
t.Fatalf("marshal test Codex client catalog: %v", err)
|
|
}
|
|
return data
|
|
}
|