vibe-proxy/backend/internal/registry/codex_client_models_test.go
2026-08-24 00:10:41 +02:00

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
}