package service import ( "encoding/json" "net/http" "net/http/httptest" "reflect" "strings" "testing" "github.com/basketikun/infinite-canvas/model" ) func TestFetchAdminChannelModelsParsesOpenAIModels(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/models" { t.Fatalf("unexpected path: %s", r.URL.Path) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[{"id":"z-model"},{"id":"a-model"},{"id":""}]}`)) })) defer server.Close() models, err := fetchAdminChannelModels(model.ModelChannel{ BaseURL: server.URL, APIKey: "test-key", }) if err != nil { t.Fatalf("fetchAdminChannelModels returned error: %v", err) } if want := []string{"a-model", "z-model"}; !reflect.DeepEqual(models, want) { t.Fatalf("models = %#v, want %#v", models, want) } } func TestFetchAdminChannelModelsReportsArkPlanModelsUnsupported(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/plan/v3/models" { t.Fatalf("unexpected path: %s", r.URL.Path) } http.NotFound(w, r) })) defer server.Close() _, err := fetchAdminChannelModels(model.ModelChannel{ BaseURL: server.URL + "/api/plan/v3/contents/generations/tasks", APIKey: "test-key", }) if err == nil { t.Fatal("expected unsupported /models error") } if !strings.Contains(err.Error(), "Agent Plan 未提供 OpenAI /models") { t.Fatalf("error = %q", err.Error()) } } func TestBuildModelChannelURLNormalizesArkPlanTaskPath(t *testing.T) { got := BuildModelChannelURL(model.ModelChannel{BaseURL: "https://ark.cn-beijing.volces.com/api/plan/v3/contents/generations/tasks?debug=1"}, "/models") want := "https://ark.cn-beijing.volces.com/api/plan/v3/models" if got != want { t.Fatalf("BuildModelChannelURL = %q, want %q", got, want) } } func TestBuildModelChannelURLAcceptsVersionedSub2APIBaseURL(t *testing.T) { got := BuildModelChannelURL(model.ModelChannel{BaseURL: "https://sub2api.example.com/v1"}, "/images/generations") want := "https://sub2api.example.com/v1/images/generations" if got != want { t.Fatalf("BuildModelChannelURL = %q, want %q", got, want) } } func TestNormalizePrivateSettingDefaultsSub2APIImageResponseFormatToURL(t *testing.T) { setting := normalizePrivateSetting(model.PrivateSetting{Channels: []model.ModelChannel{{Compatibility: "sub2api"}}}) if got := setting.Channels[0].RequestOptions.ImageResponseFormat; got != "url" { t.Fatalf("image response format = %q, want url", got) } } func TestAdminTestChannelModelUsesImageGenerationProbeForImageModels(t *testing.T) { var gotPath string var gotPayload map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path if err := json.NewDecoder(r.Body).Decode(&gotPayload); err != nil { t.Fatalf("Decode request body: %v", err) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[{"url":"https://example.com/image.png"}]}`)) })) defer server.Close() result, err := testAdminChannelModel(model.ModelChannel{ Compatibility: "sub2api", BaseURL: server.URL + "/v1", APIKey: "test-key", }, "gpt-image-2") if err != nil { t.Fatalf("testAdminChannelModel returned error: %v", err) } if result != "ok" { t.Fatalf("result = %q, want ok", result) } if gotPath != "/v1/images/generations" { t.Fatalf("path = %q, want /v1/images/generations", gotPath) } if gotPayload["model"] != "gpt-image-2" { t.Fatalf("model = %#v", gotPayload["model"]) } if gotPayload["response_format"] != "url" { t.Fatalf("response_format = %#v, want url", gotPayload["response_format"]) } } func TestNormalizeSettingsPublishesEnabledChannelModelsAndRepairsDefaults(t *testing.T) { settings := normalizeSettings(model.Settings{ Public: model.PublicSetting{ ModelChannel: model.PublicModelChannelSetting{ AvailableModels: []string{"grok-imagine-video", "disabled-model"}, DefaultModel: "grok-imagine-video", DefaultTextModel: "missing-text", DefaultImageModel: "missing-image", DefaultVideoModel: "missing-video", }, }, Private: model.PrivateSetting{ Channels: []model.ModelChannel{ {Enabled: true, Models: []string{"gpt-5.5", "doubao-seedream-5.0-lite", "doubao-seedance-2.0-fast", "gpt-5.5"}}, {Enabled: false, Models: []string{"disabled-model"}}, }, }, }) channel := settings.Public.ModelChannel wantModels := []string{"gpt-5.5", "doubao-seedream-5.0-lite", "doubao-seedance-2.0-fast"} if !reflect.DeepEqual(channel.AvailableModels, wantModels) { t.Fatalf("available models = %#v, want %#v", channel.AvailableModels, wantModels) } if channel.DefaultModel != "gpt-5.5" { t.Fatalf("default model = %q, want text model", channel.DefaultModel) } if channel.DefaultTextModel != "gpt-5.5" { t.Fatalf("default text model = %q, want text model", channel.DefaultTextModel) } if channel.DefaultImageModel != "doubao-seedream-5.0-lite" { t.Fatalf("default image model = %q, want seedream", channel.DefaultImageModel) } if channel.DefaultVideoModel != "doubao-seedance-2.0-fast" { t.Fatalf("default video model = %q, want seedance", channel.DefaultVideoModel) } }