298 lines
11 KiB
Go
298 lines
11 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"sub2api-cn-relay-manager/internal/store/sqlite"
|
|
)
|
|
|
|
func TestAPICreateLogicalGroupReturnsCreated(t *testing.T) {
|
|
handler := NewAPIHandler("secret-token", ActionSet{
|
|
CreateLogicalGroup: func(_ context.Context, req CreateLogicalGroupRequest) (LogicalGroupInfo, error) {
|
|
if req.LogicalGroupID != "gpt-shared" {
|
|
t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
|
|
}
|
|
if req.UsageScenario != "适合统一 GPT 产品入口" {
|
|
t.Fatalf("UsageScenario = %q, want configured guidance", req.UsageScenario)
|
|
}
|
|
return LogicalGroupInfo{
|
|
LogicalGroupID: req.LogicalGroupID,
|
|
DisplayName: req.DisplayName,
|
|
Status: req.Status,
|
|
UsageScenario: req.UsageScenario,
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
request := httptestRequest(t, http.MethodPost, "/api/logical-groups", map[string]any{
|
|
"logical_group_id": "gpt-shared",
|
|
"display_name": "GPT Shared",
|
|
"status": "active",
|
|
"usage_scenario": "适合统一 GPT 产品入口",
|
|
}, "secret-token")
|
|
response := httptestRecorder(handler, request)
|
|
assertStatusCode(t, response, http.StatusCreated)
|
|
assertJSONContains(t, response.Body().Bytes(), "logical_group.logical_group_id", "gpt-shared")
|
|
}
|
|
|
|
func TestAPIGetLogicalGroupReturnsAggregatedItem(t *testing.T) {
|
|
handler := NewAPIHandler("secret-token", ActionSet{
|
|
GetLogicalGroup: func(_ context.Context, groupID string) (LogicalGroupInfo, error) {
|
|
if groupID != "gpt-shared" {
|
|
t.Fatalf("groupID = %q, want gpt-shared", groupID)
|
|
}
|
|
return LogicalGroupInfo{
|
|
LogicalGroupID: groupID,
|
|
DisplayName: "GPT Shared",
|
|
Status: "active",
|
|
UsageScenario: "适合统一 GPT 产品入口",
|
|
Recommendation: "优先使用 gpt-5.4",
|
|
NextStepHint: "先创建测试 Key",
|
|
Models: []LogicalGroupModelInfo{{PublicModel: "gpt-5.4", Status: "active"}},
|
|
Routes: []LogicalGroupRouteInfo{{
|
|
RouteID: "asxs",
|
|
LogicalGroupID: groupID,
|
|
Name: "ASXS",
|
|
Status: "active",
|
|
Models: []LogicalGroupRouteModelInfo{{PublicModel: "gpt-5.4", ShadowModel: "gpt-5.4"}},
|
|
}},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
request := httptestRequest(t, http.MethodGet, "/api/logical-groups/gpt-shared", nil, "secret-token")
|
|
response := httptestRecorder(handler, request)
|
|
assertStatusCode(t, response, http.StatusOK)
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(response.Body().Bytes(), &payload); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
group, ok := payload["logical_group"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("logical_group = %#v, want object", payload["logical_group"])
|
|
}
|
|
models, ok := group["models"].([]any)
|
|
if !ok || len(models) != 1 {
|
|
t.Fatalf("models = %#v, want one item", group["models"])
|
|
}
|
|
firstModel, ok := models[0].(map[string]any)
|
|
if !ok || firstModel["public_model"] != "gpt-5.4" {
|
|
t.Fatalf("first model = %#v, want public_model gpt-5.4", models[0])
|
|
}
|
|
routes, ok := group["routes"].([]any)
|
|
if !ok || len(routes) != 1 {
|
|
t.Fatalf("routes = %#v, want one item", group["routes"])
|
|
}
|
|
firstRoute, ok := routes[0].(map[string]any)
|
|
if !ok || firstRoute["route_id"] != "asxs" {
|
|
t.Fatalf("first route = %#v, want route_id asxs", routes[0])
|
|
}
|
|
if group["usage_scenario"] != "适合统一 GPT 产品入口" || group["recommendation"] != "优先使用 gpt-5.4" || group["next_step_hint"] != "先创建测试 Key" {
|
|
t.Fatalf("group guidance = %#v, want configured guidance fields", group)
|
|
}
|
|
}
|
|
|
|
func TestAPICreateLogicalGroupRouteUsesPathGroupID(t *testing.T) {
|
|
handler := NewAPIHandler("secret-token", ActionSet{
|
|
CreateLogicalGroupRoute: func(_ context.Context, req CreateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error) {
|
|
if req.LogicalGroupID != "gpt-shared" {
|
|
t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
|
|
}
|
|
return LogicalGroupRouteInfo{
|
|
RouteID: req.RouteID,
|
|
LogicalGroupID: req.LogicalGroupID,
|
|
Name: req.Name,
|
|
Status: req.Status,
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
request := httptestRequest(t, http.MethodPost, "/api/logical-groups/gpt-shared/routes", map[string]any{
|
|
"route_id": "asxs",
|
|
"name": "ASXS",
|
|
"status": "active",
|
|
"priority": 10,
|
|
"shadow_group_id": "gpt-shared__asxs",
|
|
"shadow_host_id": "remote43",
|
|
}, "secret-token")
|
|
response := httptestRecorder(handler, request)
|
|
assertStatusCode(t, response, http.StatusCreated)
|
|
assertJSONContains(t, response.Body().Bytes(), "route.logical_group_id", "gpt-shared")
|
|
assertJSONContains(t, response.Body().Bytes(), "route.route_id", "asxs")
|
|
}
|
|
|
|
func TestAPICreateLogicalGroupRouteModelUsesPathValues(t *testing.T) {
|
|
handler := NewAPIHandler("secret-token", ActionSet{
|
|
CreateLogicalGroupRouteModel: func(_ context.Context, req CreateLogicalGroupRouteModelRequest) (LogicalGroupRouteModelInfo, error) {
|
|
if req.LogicalGroupID != "gpt-shared" {
|
|
t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
|
|
}
|
|
if req.RouteID != "asxs" {
|
|
t.Fatalf("RouteID = %q, want asxs", req.RouteID)
|
|
}
|
|
return LogicalGroupRouteModelInfo{
|
|
PublicModel: req.PublicModel,
|
|
ShadowModel: req.ShadowModel,
|
|
Status: req.Status,
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
request := httptestRequest(t, http.MethodPost, "/api/logical-groups/gpt-shared/routes/asxs/models", map[string]any{
|
|
"public_model": "gpt-5.4",
|
|
"shadow_model": "gpt-5.4",
|
|
"status": "active",
|
|
}, "secret-token")
|
|
response := httptestRecorder(handler, request)
|
|
assertStatusCode(t, response, http.StatusCreated)
|
|
assertJSONContains(t, response.Body().Bytes(), "route_model.public_model", "gpt-5.4")
|
|
}
|
|
|
|
func TestNewActionSetLogicalGroupCRUDFlow(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "logical-groups.db")
|
|
dsn := "file:" + filepath.ToSlash(dbPath) + "?_busy_timeout=5000"
|
|
actions := NewActionSet(dsn)
|
|
ctx := context.Background()
|
|
|
|
createdGroup, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
DisplayName: "GPT Shared",
|
|
Status: "active",
|
|
UsageScenario: "适合统一 GPT 产品入口",
|
|
Recommendation: "优先使用 gpt-5.4",
|
|
NextStepHint: "先创建测试 Key",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateLogicalGroup() error = %v", err)
|
|
}
|
|
if createdGroup.LogicalGroupID != "gpt-shared" {
|
|
t.Fatalf("CreateLogicalGroup() = %+v, want logical_group_id gpt-shared", createdGroup)
|
|
}
|
|
if createdGroup.UsageScenario != "适合统一 GPT 产品入口" || createdGroup.Recommendation != "优先使用 gpt-5.4" || createdGroup.NextStepHint != "先创建测试 Key" {
|
|
t.Fatalf("CreateLogicalGroup() guidance = %+v, want configured guidance fields", createdGroup)
|
|
}
|
|
|
|
if _, err := actions.CreateLogicalGroupModel(ctx, CreateLogicalGroupModelRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
PublicModel: "gpt-5.4",
|
|
}); err != nil {
|
|
t.Fatalf("CreateLogicalGroupModel() error = %v", err)
|
|
}
|
|
|
|
if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
RouteID: "asxs",
|
|
Name: "ASXS",
|
|
Status: "active",
|
|
Priority: 10,
|
|
ShadowGroupID: "gpt-shared__asxs",
|
|
ShadowHostID: "remote43",
|
|
}); err != nil {
|
|
t.Fatalf("CreateLogicalGroupRoute() error = %v", err)
|
|
}
|
|
|
|
if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
RouteID: "asxs",
|
|
PublicModel: "gpt-5.4",
|
|
}); err != nil {
|
|
t.Fatalf("CreateLogicalGroupRouteModel() error = %v", err)
|
|
}
|
|
|
|
group, err := actions.GetLogicalGroup(ctx, "gpt-shared")
|
|
if err != nil {
|
|
t.Fatalf("GetLogicalGroup() error = %v", err)
|
|
}
|
|
if len(group.Models) != 1 || group.Models[0].PublicModel != "gpt-5.4" {
|
|
t.Fatalf("GetLogicalGroup().Models = %+v, want gpt-5.4", group.Models)
|
|
}
|
|
if len(group.Routes) != 1 || group.Routes[0].RouteID != "asxs" {
|
|
t.Fatalf("GetLogicalGroup().Routes = %+v, want route asxs", group.Routes)
|
|
}
|
|
if len(group.Routes[0].Models) != 1 || group.Routes[0].Models[0].ShadowModel != "gpt-5.4" {
|
|
t.Fatalf("GetLogicalGroup().Routes[0].Models = %+v, want shadow gpt-5.4", group.Routes[0].Models)
|
|
}
|
|
if group.UsageScenario != "适合统一 GPT 产品入口" || group.Recommendation != "优先使用 gpt-5.4" || group.NextStepHint != "先创建测试 Key" {
|
|
t.Fatalf("GetLogicalGroup() guidance = %+v, want configured guidance fields", group)
|
|
}
|
|
|
|
if _, err := actions.UpdateLogicalGroup(ctx, UpdateLogicalGroupRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
DisplayName: "GPT Shared Updated",
|
|
Status: "paused",
|
|
UsageScenario: "适合升级后的 GPT 产品入口",
|
|
Recommendation: "先验证高质量推理链路",
|
|
NextStepHint: "升级后重新申请测试 Key",
|
|
}); err != nil {
|
|
t.Fatalf("UpdateLogicalGroup() error = %v", err)
|
|
}
|
|
if _, err := actions.UpdateLogicalGroupRoute(ctx, UpdateLogicalGroupRouteRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
RouteID: "asxs",
|
|
Name: "ASXS Updated",
|
|
Status: "degraded",
|
|
Priority: 20,
|
|
Weight: 80,
|
|
ShadowGroupID: "gpt-shared__asxs",
|
|
ShadowHostID: "remote43",
|
|
CooldownUntil: "2026-05-28T16:00:00Z",
|
|
}); err != nil {
|
|
t.Fatalf("UpdateLogicalGroupRoute() error = %v", err)
|
|
}
|
|
|
|
groups, err := actions.ListLogicalGroups(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListLogicalGroups() error = %v", err)
|
|
}
|
|
if len(groups) != 1 || groups[0].DisplayName != "GPT Shared Updated" {
|
|
t.Fatalf("ListLogicalGroups() = %+v, want updated group", groups)
|
|
}
|
|
if groups[0].UsageScenario != "适合升级后的 GPT 产品入口" || groups[0].Recommendation != "先验证高质量推理链路" || groups[0].NextStepHint != "升级后重新申请测试 Key" {
|
|
t.Fatalf("ListLogicalGroups() guidance = %+v, want updated guidance fields", groups[0])
|
|
}
|
|
|
|
routeModels, err := actions.ListLogicalGroupRouteModels(ctx, ListLogicalGroupRouteModelsRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
RouteID: "asxs",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListLogicalGroupRouteModels() error = %v", err)
|
|
}
|
|
if len(routeModels) != 1 || routeModels[0].PublicModel != "gpt-5.4" {
|
|
t.Fatalf("ListLogicalGroupRouteModels() = %+v, want gpt-5.4", routeModels)
|
|
}
|
|
|
|
if err := actions.DeleteLogicalGroupRoute(ctx, DeleteLogicalGroupRouteRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
RouteID: "asxs",
|
|
}); err != nil {
|
|
t.Fatalf("DeleteLogicalGroupRoute() error = %v", err)
|
|
}
|
|
if err := actions.DeleteLogicalGroupModel(ctx, DeleteLogicalGroupModelRequest{
|
|
LogicalGroupID: "gpt-shared",
|
|
PublicModel: "gpt-5.4",
|
|
}); err != nil {
|
|
t.Fatalf("DeleteLogicalGroupModel() error = %v", err)
|
|
}
|
|
if err := actions.DeleteLogicalGroup(ctx, "gpt-shared"); err != nil {
|
|
t.Fatalf("DeleteLogicalGroup() error = %v", err)
|
|
}
|
|
|
|
store, err := sqlite.Open(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("sqlite.Open() error = %v", err)
|
|
}
|
|
defer store.Close()
|
|
remaining, err := store.LogicalGroups().List(ctx)
|
|
if err != nil {
|
|
t.Fatalf("LogicalGroups().List() error = %v", err)
|
|
}
|
|
if len(remaining) != 0 {
|
|
t.Fatalf("remaining logical groups = %+v, want empty", remaining)
|
|
}
|
|
}
|