fix fresh-host acceptance and document real-host debugging learnings

This commit is contained in:
phamnazage-jpg
2026-05-21 21:19:19 +08:00
parent 7c6e18f94d
commit 3ba3244ea6
85 changed files with 1721 additions and 162 deletions

View File

@@ -228,34 +228,43 @@ func TestFilterNamedResourcesByPrefix(t *testing.T) {
func TestDecodeNamedResources(t *testing.T) {
t.Run("envelope", func(t *testing.T) {
resources, err := decodeNamedResources([]byte(`{"data":[{"id":"r1","name":"n1"}]}`))
resources, pages, err := decodeNamedResources([]byte(`{"data":[{"id":"r1","name":"n1"}]}`))
if err != nil {
t.Fatal(err)
}
if pages != 1 {
t.Fatalf("pages = %d, want 1", pages)
}
if len(resources) != 1 || resources[0].ID != "r1" {
t.Fatalf("got %+v", resources)
}
})
t.Run("numeric id", func(t *testing.T) {
resources, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":1,"name":"default"}]}}`))
resources, pages, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":1,"name":"default"}],"pages":2}}`))
if err != nil {
t.Fatal(err)
}
if pages != 2 {
t.Fatalf("pages = %d, want 2", pages)
}
if len(resources) != 1 || resources[0].ID != "1" {
t.Fatalf("got %+v", resources)
}
})
t.Run("wrapper with items", func(t *testing.T) {
resources, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":"r2","name":"n2"}]}}`))
resources, pages, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":"r2","name":"n2"}]}}`))
if err != nil {
t.Fatal(err)
}
if pages != 1 {
t.Fatalf("pages = %d, want 1", pages)
}
if len(resources) != 1 || resources[0].ID != "r2" {
t.Fatalf("got %+v", resources)
}
})
t.Run("invalid json", func(t *testing.T) {
_, err := decodeNamedResources([]byte(`not json`))
_, _, err := decodeNamedResources([]byte(`not json`))
if err == nil {
t.Fatal("expected error")
}
@@ -904,6 +913,12 @@ func TestEnsureSubscriptionAccessWithMock(t *testing.T) {
func TestCheckGatewayAccessWithMock(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer gk" {
t.Fatalf("Authorization = %q, want %q", got, "Bearer gk")
}
if got := r.Header.Get("x-api-key"); got != "" {
t.Fatalf("x-api-key = %q, want empty", got)
}
w.Write([]byte(`{"data":[{"id":"gpt-4"},{"id":"claude-3"}]}`))
}))
defer srv.Close()
@@ -920,6 +935,50 @@ func TestCheckGatewayAccessWithMock(t *testing.T) {
}
}
func TestCheckGatewayCompletionWithMock(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/chat/completions" {
t.Fatalf("path = %q, want /v1/chat/completions", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer gk" {
t.Fatalf("Authorization = %q, want %q", got, "Bearer gk")
}
if got := r.Header.Get("x-api-key"); got != "" {
t.Fatalf("x-api-key = %q, want empty", got)
}
var payload struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if payload.Model != "gpt-4" {
t.Fatalf("model = %q, want gpt-4", payload.Model)
}
if payload.MaxTokens != 8 {
t.Fatalf("max_tokens = %d, want 8", payload.MaxTokens)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"choices":[{"message":{"content":"pong"}}]}`))
}))
defer srv.Close()
client, _ := NewClient(srv.URL, WithAPIKey("k"))
result, err := client.CheckGatewayCompletion(context.Background(), GatewayCompletionCheckRequest{APIKey: "gk", Model: "gpt-4"})
if err != nil {
t.Fatal(err)
}
if !result.OK {
t.Fatal("expected completion OK=true")
}
if result.StatusCode != 200 {
t.Fatalf("status = %d, want 200", result.StatusCode)
}
if result.ContentType != "application/json" {
t.Fatalf("content type = %q, want application/json", result.ContentType)
}
}
func TestBatchCreateAccountsWithMock(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
@@ -1037,19 +1096,93 @@ func TestListManagedResourcesWithMock(t *testing.T) {
}
}
func TestTestAccountWithMock(t *testing.T) {
func TestListManagedResourcesLoadsAllAccountPages(t *testing.T) {
accountPages := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/admin/groups", "/api/v1/admin/channels":
_, _ = w.Write([]byte(`{"data":{"items":[{"id":"r1","name":"resource-1"}],"total":1,"page":1,"page_size":20,"pages":1}}`))
case "/api/v1/admin/payment/plans":
_, _ = w.Write([]byte(`{"data":[{"id":"plan_1","name":"plan-1"}]}`))
case "/api/v1/admin/accounts":
accountPages++
page := r.URL.Query().Get("page")
if page == "" {
page = "1"
}
if got := r.URL.Query().Get("page_size"); got != "100" {
t.Fatalf("page_size = %q, want 100", got)
}
switch page {
case "1":
_, _ = w.Write([]byte(`{"data":{"items":[{"id":"account_1","name":"deepseek-01"}],"total":2,"page":1,"page_size":100,"pages":2}}`))
case "2":
_, _ = w.Write([]byte(`{"data":{"items":[{"id":"account_2","name":"deepseek-02"}],"total":2,"page":2,"page_size":100,"pages":2}}`))
default:
t.Fatalf("unexpected accounts page %q", page)
}
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
client, _ := NewClient(srv.URL, WithAPIKey("k"))
snapshot, err := client.ListManagedResources(context.Background(), ListManagedResourcesRequest{AccountNamePrefix: "deepseek-"})
if err != nil {
t.Fatal(err)
}
if accountPages != 2 {
t.Fatalf("account pages fetched = %d, want 2", accountPages)
}
if len(snapshot.Accounts) != 2 || snapshot.Accounts[0].ID != "account_1" || snapshot.Accounts[1].ID != "account_2" {
t.Fatalf("Accounts = %+v, want both paged accounts", snapshot.Accounts)
}
}
func TestTestAccountWithMock(t *testing.T) {
var requestBody map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
t.Fatalf("decode request body: %v", err)
}
w.Write([]byte("data: {\"status\":\"passed\",\"ok\":true}\n"))
}))
defer srv.Close()
client, _ := NewClient(srv.URL, WithAPIKey("k"))
result, err := client.TestAccount(context.Background(), "a1")
result, err := client.TestAccount(context.Background(), "a1", "MiniMax-M2.7-highspeed")
if err != nil {
t.Fatal(err)
}
if !result.OK {
t.Fatal("expected OK=true")
}
if got := requestBody["model_id"]; got != "MiniMax-M2.7-highspeed" {
t.Fatalf("model_id = %#v, want MiniMax-M2.7-highspeed", got)
}
}
func TestTestAccountWithMockSSEError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Write([]byte("data: {\"type\":\"test_start\",\"model\":\"MiniMax-M2.7-highspeed\"}\n\n"))
w.Write([]byte("data: {\"type\":\"error\",\"error\":\"账号本身可正常使用,但当前测试接口仅支持 Responses API 路径。请直接通过实际 API 调用验证。\"}\n\n"))
}))
defer srv.Close()
client, _ := NewClient(srv.URL, WithAPIKey("k"))
result, err := client.TestAccount(context.Background(), "a1", "")
if err != nil {
t.Fatal(err)
}
if result.OK {
t.Fatal("expected OK=false for SSE error event")
}
if result.Status != "failed" {
t.Fatalf("Status = %q, want failed", result.Status)
}
if !strings.Contains(result.Message, "测试接口仅支持 Responses API 路径") {
t.Fatalf("Message = %q, want propagated SSE error message", result.Message)
}
}
func TestGetAccountModelsWithMock(t *testing.T) {