mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 06:10:46 +00:00
30708ae79d
* feat(auth): support named chatgpt oauth providers - add provider-scoped ChatGPT OAuth routes and CLI support - persist refresh tokens per provider and reject provider-type collisions - wire provider OAuth setup flows in the dashboard and setup UI Refs #448 * feat(agent): add chatgpt oauth account routing - add agent other_config routing for manual and round-robin selection - reuse routed provider resolution across resolver and pending loaders - add router, parser, and agent advanced dialog coverage for multi-account use Refs #448 * docs(api): describe chatgpt oauth routing - document named-provider ChatGPT OAuth auth routes - describe agent-side account routing and round-robin behavior - update OpenAPI agent config schema and provider type enum Refs #448 * fix(store): add missing agent key context helpers * feat(ui): clarify chatgpt oauth account setup and routing * docs(providers): align chatgpt oauth alias examples * feat(agent): add codex pool activity dashboard * fix(providers): harden codex oauth alias setup * feat(codex-pool): improve routing dashboard UX - redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence - add clearer selection, attention, and recent-proof states for pool members - make the lower panels fill the remaining desktop viewport while staying responsive * fix(store): resolve context helper merge duplication * feat(oauth): add codex pool quota and observation APIs - add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers - teach codex routing to surface pool activity, observation metadata, and quota-aware readiness - extend tests and HTTP docs/OpenAPI for the new pool monitoring flows * feat(web): add codex pool quota monitor and controls - add provider quota fetching, readiness badges, and live routing evidence on the account pool page - redesign pool setup and activity panels for multi-account management with localized copy updates - keep the live monitor internally scrollable and compact the account cards for better viewport fit * fix(web): clarify pool routing labels - rename the recent request badge from Direct to Selected - restore compact quota bars in the live pool cards * feat(codex-pool): add runtime health dashboard - derive per-provider success and failure health from routed Codex traces - surface routing, quota, and recent request evidence in the pool UI - align provider alias guidance and owner access with the dashboard role model * docs(auth): document tenant scoping and key roles * fix(auth): harden tenant and codex pool access control * fix(providers): align codex pool runtime defaults * feat(ui): tighten codex pool responsive layout * feat(chatgpt-oauth): refine codex pool management UX * feat(chatgpt-oauth): surface quota bars on provider pages - add compact quota bars to Codex provider rows and provider detail - fetch quota only for ready visible provider rows and ready detail aliases - fix managed-member detail visibility and tighten provider locale copy
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
type testTokenSource struct {
|
|
token string
|
|
}
|
|
|
|
func (s *testTokenSource) Token() (string, error) {
|
|
return s.token, nil
|
|
}
|
|
|
|
func (s *testTokenSource) RouteEligibility(context.Context) providers.RouteEligibility {
|
|
return providers.RouteEligibility{Class: providers.RouteEligibilityHealthy}
|
|
}
|
|
|
|
func TestResolveCodexPoolRoutingUsesProviderDefaults(t *testing.T) {
|
|
providers := newMockProviderStore()
|
|
tenantID := uuid.New()
|
|
if err := providers.CreateProvider(context.Background(), &store.LLMProviderData{
|
|
BaseModel: store.BaseModel{ID: uuid.New()},
|
|
TenantID: tenantID,
|
|
Name: "openai-codex",
|
|
ProviderType: store.ProviderChatGPTOAuth,
|
|
Enabled: true,
|
|
Settings: json.RawMessage(`{
|
|
"codex_pool": {
|
|
"strategy": "round_robin",
|
|
"extra_provider_names": ["codex-work"]
|
|
}
|
|
}`),
|
|
}); err != nil {
|
|
t.Fatalf("CreateProvider() error = %v", err)
|
|
}
|
|
|
|
agent := &store.AgentData{
|
|
TenantID: tenantID,
|
|
Provider: "openai-codex",
|
|
}
|
|
|
|
providerType, routing, poolProviders := resolveCodexPoolRouting(context.Background(), providers, nil, agent)
|
|
if providerType != store.ProviderChatGPTOAuth {
|
|
t.Fatalf("providerType = %q, want %q", providerType, store.ProviderChatGPTOAuth)
|
|
}
|
|
if routing == nil {
|
|
t.Fatal("routing = nil, want effective routing")
|
|
}
|
|
if routing.Strategy != store.ChatGPTOAuthStrategyRoundRobin {
|
|
t.Fatalf("Strategy = %q, want %q", routing.Strategy, store.ChatGPTOAuthStrategyRoundRobin)
|
|
}
|
|
if len(routing.ExtraProviderNames) != 1 || routing.ExtraProviderNames[0] != "codex-work" {
|
|
t.Fatalf("ExtraProviderNames = %#v, want [\"codex-work\"]", routing.ExtraProviderNames)
|
|
}
|
|
if len(poolProviders) != 2 || poolProviders[0] != "openai-codex" || poolProviders[1] != "codex-work" {
|
|
t.Fatalf("poolProviders = %#v, want primary + extra", poolProviders)
|
|
}
|
|
}
|
|
|
|
func TestResolveCodexPoolRoutingHonorsInheritOverride(t *testing.T) {
|
|
providers := newMockProviderStore()
|
|
tenantID := uuid.New()
|
|
if err := providers.CreateProvider(context.Background(), &store.LLMProviderData{
|
|
BaseModel: store.BaseModel{ID: uuid.New()},
|
|
TenantID: tenantID,
|
|
Name: "openai-codex",
|
|
ProviderType: store.ProviderChatGPTOAuth,
|
|
Enabled: true,
|
|
Settings: json.RawMessage(`{
|
|
"codex_pool": {
|
|
"strategy": "priority_order",
|
|
"extra_provider_names": ["codex-team"]
|
|
}
|
|
}`),
|
|
}); err != nil {
|
|
t.Fatalf("CreateProvider() error = %v", err)
|
|
}
|
|
|
|
agent := &store.AgentData{
|
|
TenantID: tenantID,
|
|
Provider: "openai-codex",
|
|
OtherConfig: json.RawMessage(`{
|
|
"chatgpt_oauth_routing": {
|
|
"override_mode": "inherit",
|
|
"strategy": "round_robin",
|
|
"extra_provider_names": ["ignored-backup"]
|
|
}
|
|
}`),
|
|
}
|
|
|
|
_, routing, poolProviders := resolveCodexPoolRouting(context.Background(), providers, nil, agent)
|
|
if routing == nil {
|
|
t.Fatal("routing = nil, want inherited routing")
|
|
}
|
|
if routing.Strategy != store.ChatGPTOAuthStrategyPriority {
|
|
t.Fatalf("Strategy = %q, want %q", routing.Strategy, store.ChatGPTOAuthStrategyPriority)
|
|
}
|
|
if len(routing.ExtraProviderNames) != 1 || routing.ExtraProviderNames[0] != "codex-team" {
|
|
t.Fatalf("ExtraProviderNames = %#v, want [\"codex-team\"]", routing.ExtraProviderNames)
|
|
}
|
|
if len(poolProviders) != 2 || poolProviders[1] != "codex-team" {
|
|
t.Fatalf("poolProviders = %#v, want inherited pool order", poolProviders)
|
|
}
|
|
}
|
|
|
|
func TestResolveCodexPoolRoutingIgnoresNonCodexBaseProvider(t *testing.T) {
|
|
providers := newMockProviderStore()
|
|
tenantID := uuid.New()
|
|
if err := providers.CreateProvider(context.Background(), &store.LLMProviderData{
|
|
BaseModel: store.BaseModel{ID: uuid.New()},
|
|
TenantID: tenantID,
|
|
Name: "anthropic",
|
|
ProviderType: store.ProviderAnthropicNative,
|
|
Enabled: true,
|
|
}); err != nil {
|
|
t.Fatalf("CreateProvider() error = %v", err)
|
|
}
|
|
|
|
agent := &store.AgentData{
|
|
TenantID: tenantID,
|
|
Provider: "anthropic",
|
|
OtherConfig: json.RawMessage(`{
|
|
"chatgpt_oauth_routing": {
|
|
"strategy": "round_robin",
|
|
"extra_provider_names": ["codex-backup"]
|
|
}
|
|
}`),
|
|
}
|
|
|
|
providerType, routing, poolProviders := resolveCodexPoolRouting(context.Background(), providers, nil, agent)
|
|
if providerType != store.ProviderAnthropicNative {
|
|
t.Fatalf("providerType = %q, want %q", providerType, store.ProviderAnthropicNative)
|
|
}
|
|
if routing != nil {
|
|
t.Fatalf("routing = %#v, want nil for non-Codex provider", routing)
|
|
}
|
|
if len(poolProviders) != 0 {
|
|
t.Fatalf("poolProviders = %#v, want empty for non-Codex provider", poolProviders)
|
|
}
|
|
}
|
|
|
|
func TestResolveCodexPoolRoutingUsesRegistryMasterFallback(t *testing.T) {
|
|
tenantID := uuid.New()
|
|
registry := providers.NewRegistry(nil)
|
|
registry.RegisterForTenant(providers.MasterTenantID, providers.NewCodexProvider(
|
|
"openai-codex",
|
|
&testTokenSource{token: "primary-token"},
|
|
"http://127.0.0.1",
|
|
"gpt-5.4",
|
|
).WithRoutingDefaults(store.ChatGPTOAuthStrategyRoundRobin, []string{"codex-work"}))
|
|
registry.RegisterForTenant(tenantID, providers.NewCodexProvider(
|
|
"codex-work",
|
|
&testTokenSource{token: "backup-token"},
|
|
"http://127.0.0.1",
|
|
"gpt-5.4",
|
|
))
|
|
|
|
agent := &store.AgentData{
|
|
TenantID: tenantID,
|
|
Provider: "openai-codex",
|
|
}
|
|
|
|
providerType, routing, poolProviders := resolveCodexPoolRouting(context.Background(), nil, registry, agent)
|
|
if providerType != store.ProviderChatGPTOAuth {
|
|
t.Fatalf("providerType = %q, want %q", providerType, store.ProviderChatGPTOAuth)
|
|
}
|
|
if routing == nil {
|
|
t.Fatal("routing = nil, want effective routing")
|
|
}
|
|
if routing.Strategy != store.ChatGPTOAuthStrategyRoundRobin {
|
|
t.Fatalf("Strategy = %q, want %q", routing.Strategy, store.ChatGPTOAuthStrategyRoundRobin)
|
|
}
|
|
if len(poolProviders) != 2 || poolProviders[0] != "openai-codex" || poolProviders[1] != "codex-work" {
|
|
t.Fatalf("poolProviders = %#v, want master fallback pool", poolProviders)
|
|
}
|
|
}
|