Files
goclaw/internal/http/agents_codex_pool_activity_test.go
viettranx 74dc086a80 fix(providers): post-merge fixes for Codex OAuth pools (#450)
- fix Rules of Hooks violation in chatgpt-oauth-routing-section
- add stale-while-revalidate with atomic dedup for RouteEligibility
- move raw SQL from HTTP handler to TracingStore.ListCodexPoolSpans
- persist round-robin state in Registry shared counter
- extract duplicated frontend helpers to agent-display-utils
- split oversized frontend files (964→214 lines max)
- add GIN indexes for spans.metadata and sessions.metadata
- fix tenant-aware provider lookup in handleQuota
- separate empty-role vs error handling in resolveTenantHint
- scope pool validation to chatgpt_oauth providers only
- wrap buildEntries in useCallback for stable useMemo deps
- document OAuth concurrent auth limitation and RoleAdmin breaking change
2026-03-27 10:19:16 +07:00

147 lines
5.0 KiB
Go

package http
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
func TestBuildCodexPoolActivitySeparatesDirectSelectionAndFailover(t *testing.T) {
now := time.Date(2026, 3, 24, 20, 0, 0, 0, time.UTC)
directA := providers.MergeChatGPTOAuthRoutingMetadata(nil, providers.ChatGPTOAuthRoutingEvidence{
Strategy: "round_robin",
PoolProviders: []string{"pool-a", "pool-b", "pool-c"},
SelectedProvider: "pool-a",
ServingProvider: "pool-a",
AttemptCount: 1,
})
directB := providers.MergeChatGPTOAuthRoutingMetadata(nil, providers.ChatGPTOAuthRoutingEvidence{
Strategy: "round_robin",
PoolProviders: []string{"pool-a", "pool-b", "pool-c"},
SelectedProvider: "pool-b",
ServingProvider: "pool-b",
AttemptCount: 1,
})
failoverToB := providers.MergeChatGPTOAuthRoutingMetadata(nil, providers.ChatGPTOAuthRoutingEvidence{
Strategy: "round_robin",
PoolProviders: []string{"pool-a", "pool-b", "pool-c"},
SelectedProvider: "pool-a",
ServingProvider: "pool-b",
AttemptedProviders: []string{"pool-a", "pool-b"},
FailoverProviders: []string{"pool-b"},
AttemptCount: 2,
})
providerCounts, recent := buildCodexPoolActivity([]string{"pool-a", "pool-b", "pool-c"}, []store.CodexPoolSpan{
{
SpanID: uuid.New(),
TraceID: uuid.New(),
StartedAt: now,
DurationMS: 450,
Status: "completed",
Provider: "pool-a",
Model: "gpt-5.4",
Metadata: directA,
},
{
SpanID: uuid.New(),
TraceID: uuid.New(),
StartedAt: now.Add(1 * time.Minute),
DurationMS: 500,
Status: "completed",
Provider: "pool-b",
Model: "gpt-5.4",
Metadata: directB,
},
{
SpanID: uuid.New(),
TraceID: uuid.New(),
StartedAt: now.Add(2 * time.Minute),
DurationMS: 620,
Status: "completed",
Provider: "pool-b",
Model: "gpt-5.4",
Metadata: failoverToB,
},
})
if len(providerCounts) != 3 {
t.Fatalf("len(providerCounts) = %d, want 3", len(providerCounts))
}
if providerCounts[0].ProviderName != "pool-a" || providerCounts[0].DirectSelectionCount != 2 || providerCounts[0].FailoverServeCount != 0 {
t.Fatalf("pool-a counts = %#v", providerCounts[0])
}
if providerCounts[0].SuccessCount != 1 || providerCounts[0].FailureCount != 1 || providerCounts[0].ConsecutiveFailures != 1 {
t.Fatalf("pool-a runtime = %#v", providerCounts[0])
}
if providerCounts[1].ProviderName != "pool-b" || providerCounts[1].DirectSelectionCount != 1 || providerCounts[1].FailoverServeCount != 1 {
t.Fatalf("pool-b counts = %#v", providerCounts[1])
}
if providerCounts[1].SuccessCount != 2 || providerCounts[1].FailureCount != 0 || providerCounts[1].HealthState != "healthy" {
t.Fatalf("pool-b runtime = %#v", providerCounts[1])
}
if providerCounts[2].ProviderName != "pool-c" || providerCounts[2].DirectSelectionCount != 0 || providerCounts[2].FailoverServeCount != 0 {
t.Fatalf("pool-c counts = %#v", providerCounts[2])
}
if providerCounts[2].HealthState != "idle" || providerCounts[2].HealthScore != 0 {
t.Fatalf("pool-c health = %#v", providerCounts[2])
}
if len(recent) != 3 {
t.Fatalf("len(recent) = %d, want 3", len(recent))
}
last := recent[0]
if !last.UsedFailover {
t.Fatal("latest.UsedFailover = false, want true")
}
if last.SelectedProvider != "pool-a" || last.ProviderName != "pool-b" {
t.Fatalf("latest routing = selected %q provider %q", last.SelectedProvider, last.ProviderName)
}
if last.AttemptCount != 2 {
t.Fatalf("latest.AttemptCount = %d, want 2", last.AttemptCount)
}
}
func TestBuildCodexPoolActivityTracksTerminalFailuresAcrossAttempts(t *testing.T) {
now := time.Date(2026, 3, 24, 22, 0, 0, 0, time.UTC)
failedAttempt := providers.MergeChatGPTOAuthRoutingMetadata(nil, providers.ChatGPTOAuthRoutingEvidence{
Strategy: "round_robin",
PoolProviders: []string{"pool-a", "pool-b"},
SelectedProvider: "pool-a",
AttemptedProviders: []string{"pool-a", "pool-b"},
AttemptCount: 2,
})
providerCounts, recent := buildCodexPoolActivity([]string{"pool-a", "pool-b"}, []store.CodexPoolSpan{
{
SpanID: uuid.New(),
TraceID: uuid.New(),
StartedAt: now,
DurationMS: 900,
Status: "error",
Provider: "pool-a",
Model: "gpt-5.4",
Metadata: failedAttempt,
},
})
if len(recent) != 1 || recent[0].Status != "error" {
t.Fatalf("recent = %#v", recent)
}
if recent[0].ProviderName != "" {
t.Fatalf("recent[0].ProviderName = %q, want empty", recent[0].ProviderName)
}
if providerCounts[0].FailureCount != 1 || providerCounts[0].ConsecutiveFailures != 1 {
t.Fatalf("pool-a failure stats = %#v", providerCounts[0])
}
if providerCounts[1].FailureCount != 1 || providerCounts[1].ConsecutiveFailures != 1 {
t.Fatalf("pool-b failure stats = %#v", providerCounts[1])
}
if providerCounts[0].HealthState != "critical" || providerCounts[1].HealthState != "critical" {
t.Fatalf("health states = %#v %#v", providerCounts[0], providerCounts[1])
}
}