package sqlite import ( "context" "database/sql" "errors" "testing" ) func TestLogicalGroupsRepoCreateGetUpdateDelete(t *testing.T) { store := openTestDB(t) ctx := context.Background() id, err := store.LogicalGroups().Create(ctx, LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", Description: "shared group", }) if err != nil { t.Fatalf("Create() error = %v", err) } if id <= 0 { t.Fatalf("Create() id = %d, want positive", id) } group, err := store.LogicalGroups().GetByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("GetByLogicalGroupID() error = %v", err) } if group.RoutePolicy != defaultLogicalGroupRoutePolicy { t.Fatalf("RoutePolicy = %q, want %q", group.RoutePolicy, defaultLogicalGroupRoutePolicy) } if group.StickyMode != defaultLogicalGroupStickyMode { t.Fatalf("StickyMode = %q, want %q", group.StickyMode, defaultLogicalGroupStickyMode) } if err := store.LogicalGroups().UpdateByLogicalGroupID(ctx, LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared Updated", Status: "paused", Description: "updated", RoutePolicy: "priority", StickyMode: "user_preferred", ConversationTTLSeconds: 3600, UserModelTTLSeconds: 900, FailoverThreshold: 3, CooldownSeconds: 120, }); err != nil { t.Fatalf("UpdateByLogicalGroupID() error = %v", err) } updated, err := store.LogicalGroups().GetByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("GetByLogicalGroupID(updated) error = %v", err) } if updated.DisplayName != "GPT Shared Updated" || updated.Status != "paused" { t.Fatalf("updated group = %+v, want updated fields", updated) } if err := store.LogicalGroups().DeleteByLogicalGroupID(ctx, "gpt-shared"); err != nil { t.Fatalf("DeleteByLogicalGroupID() error = %v", err) } _, err = store.LogicalGroups().GetByLogicalGroupID(ctx, "gpt-shared") if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("GetByLogicalGroupID() after delete error = %v, want sql.ErrNoRows", err) } } func TestLogicalGroupsRepoList(t *testing.T) { store := openTestDB(t) ctx := context.Background() for _, group := range []LogicalGroup{ {LogicalGroupID: "group-a", DisplayName: "Group A", Status: "active"}, {LogicalGroupID: "group-b", DisplayName: "Group B", Status: "active"}, } { if _, err := store.LogicalGroups().Create(ctx, group); err != nil { t.Fatalf("Create(%q) error = %v", group.LogicalGroupID, err) } } groups, err := store.LogicalGroups().List(ctx) if err != nil { t.Fatalf("List() error = %v", err) } if len(groups) != 2 { t.Fatalf("List() len = %d, want 2", len(groups)) } if groups[0].LogicalGroupID != "group-a" || groups[1].LogicalGroupID != "group-b" { t.Fatalf("List() = %+v, want insertion order", groups) } } func TestLogicalGroupModelsRepoCreateListDelete(t *testing.T) { store := openTestDBWithFK(t) ctx := context.Background() if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", }); err != nil { t.Fatalf("LogicalGroups().Create() error = %v", err) } if _, err := store.LogicalGroupModels().Create(ctx, LogicalGroupModel{ LogicalGroupID: "gpt-shared", PublicModel: "gpt-5.4", }); err != nil { t.Fatalf("LogicalGroupModels().Create() error = %v", err) } models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("ListByLogicalGroupID() error = %v", err) } if len(models) != 1 || models[0].PublicModel != "gpt-5.4" { t.Fatalf("ListByLogicalGroupID() = %+v, want gpt-5.4", models) } if err := store.LogicalGroupModels().DeleteByLogicalGroupIDAndModel(ctx, "gpt-shared", "gpt-5.4"); err != nil { t.Fatalf("DeleteByLogicalGroupIDAndModel() error = %v", err) } models, err = store.LogicalGroupModels().ListByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("ListByLogicalGroupID() after delete error = %v", err) } if len(models) != 0 { t.Fatalf("ListByLogicalGroupID() after delete len = %d, want 0", len(models)) } } func TestLogicalGroupRoutesRepoCreateGetListUpdateDelete(t *testing.T) { store := openTestDBWithFK(t) ctx := context.Background() if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", }); err != nil { t.Fatalf("LogicalGroups().Create() error = %v", err) } if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{ RouteID: "asxs", LogicalGroupID: "gpt-shared", Name: "ASXS", Status: "active", Priority: 10, ShadowGroupID: "gpt-shared__asxs", ShadowHostID: "remote43", }); err != nil { t.Fatalf("LogicalGroupRoutes().Create() error = %v", err) } route, err := store.LogicalGroupRoutes().GetByRouteID(ctx, "asxs") if err != nil { t.Fatalf("GetByRouteID() error = %v", err) } if route.Weight != 100 { t.Fatalf("Weight = %d, want 100", route.Weight) } routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("ListByLogicalGroupID() error = %v", err) } if len(routes) != 1 || routes[0].RouteID != "asxs" { t.Fatalf("ListByLogicalGroupID() = %+v, want route asxs", routes) } if err := store.LogicalGroupRoutes().UpdateByRouteID(ctx, LogicalGroupRoute{ RouteID: "asxs", LogicalGroupID: "gpt-shared", Name: "ASXS Updated", Status: "degraded", Priority: 20, Weight: 80, ShadowGroupID: "gpt-shared__asxs", ShadowHostID: "remote43", UpstreamBaseURLHint: "https://api.asxs.top/v1", CooldownUntil: "2026-05-28T16:00:00Z", }); err != nil { t.Fatalf("UpdateByRouteID() error = %v", err) } updated, err := store.LogicalGroupRoutes().GetByRouteID(ctx, "asxs") if err != nil { t.Fatalf("GetByRouteID(updated) error = %v", err) } if updated.Name != "ASXS Updated" || updated.Status != "degraded" || updated.Weight != 80 { t.Fatalf("updated route = %+v, want updated fields", updated) } if err := store.LogicalGroupRoutes().DeleteByRouteID(ctx, "asxs"); err != nil { t.Fatalf("DeleteByRouteID() error = %v", err) } _, err = store.LogicalGroupRoutes().GetByRouteID(ctx, "asxs") if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("GetByRouteID() after delete error = %v, want sql.ErrNoRows", err) } } func TestLogicalGroupRouteModelsRepoCreateList(t *testing.T) { store := openTestDBWithFK(t) ctx := context.Background() if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", }); err != nil { t.Fatalf("LogicalGroups().Create() error = %v", err) } if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{ RouteID: "codex2api", LogicalGroupID: "gpt-shared", Name: "Codex2API", Status: "active", Priority: 20, ShadowGroupID: "gpt-shared__codex2api", ShadowHostID: "remote43", }); err != nil { t.Fatalf("LogicalGroupRoutes().Create() error = %v", err) } if _, err := store.LogicalGroupRouteModels().Create(ctx, LogicalGroupRouteModel{ RouteID: "codex2api", PublicModel: "gpt-5.4", }); err != nil { t.Fatalf("LogicalGroupRouteModels().Create() error = %v", err) } models, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, "codex2api") if err != nil { t.Fatalf("ListByRouteID() error = %v", err) } if len(models) != 1 { t.Fatalf("ListByRouteID() len = %d, want 1", len(models)) } if models[0].ShadowModel != "gpt-5.4" { t.Fatalf("ShadowModel = %q, want default public model", models[0].ShadowModel) } } func TestLogicalGroupReposEnforceForeignKeysAndCascadeDelete(t *testing.T) { store := openTestDBWithFK(t) ctx := context.Background() if _, err := store.LogicalGroupModels().Create(ctx, LogicalGroupModel{ LogicalGroupID: "missing-group", PublicModel: "gpt-5.4", }); err == nil { t.Fatal("LogicalGroupModels().Create() error = nil, want foreign key failure") } if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", }); err != nil { t.Fatalf("LogicalGroups().Create() error = %v", err) } if _, err := store.LogicalGroupModels().Create(ctx, LogicalGroupModel{ LogicalGroupID: "gpt-shared", PublicModel: "gpt-5.4", }); err != nil { t.Fatalf("LogicalGroupModels().Create() error = %v", err) } if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{ RouteID: "asxs", LogicalGroupID: "gpt-shared", Name: "ASXS", Status: "active", Priority: 10, ShadowGroupID: "gpt-shared__asxs", ShadowHostID: "remote43", }); err != nil { t.Fatalf("LogicalGroupRoutes().Create() error = %v", err) } if _, err := store.LogicalGroupRouteModels().Create(ctx, LogicalGroupRouteModel{ RouteID: "asxs", PublicModel: "gpt-5.4", }); err != nil { t.Fatalf("LogicalGroupRouteModels().Create() error = %v", err) } if err := store.LogicalGroups().DeleteByLogicalGroupID(ctx, "gpt-shared"); err != nil { t.Fatalf("DeleteByLogicalGroupID() error = %v", err) } models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("ListByLogicalGroupID() after cascade error = %v", err) } if len(models) != 0 { t.Fatalf("models after cascade len = %d, want 0", len(models)) } routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, "gpt-shared") if err != nil { t.Fatalf("ListByLogicalGroupID(routes) after cascade error = %v", err) } if len(routes) != 0 { t.Fatalf("routes after cascade len = %d, want 0", len(routes)) } }