Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
)
|
||||
|
||||
func TestGetOpsAdvancedSettings_DefaultSnapshotHidesOpenAITokenStats(t *testing.T) {
|
||||
repo := newRuntimeSettingRepoStub()
|
||||
svc := &OpsService{settingRepo: repo}
|
||||
|
||||
cfg, err := svc.GetOpsAdvancedSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
|
||||
}
|
||||
if cfg.DisplayOpenAITokenStats {
|
||||
t.Fatalf("DisplayOpenAITokenStats = true, want false by default")
|
||||
}
|
||||
if !cfg.DisplayAlertEvents {
|
||||
t.Fatalf("DisplayAlertEvents = false, want true by default")
|
||||
}
|
||||
if repo.getValueCalls != 0 || repo.getMultipleCalls != 0 {
|
||||
t.Fatalf("hot-path snapshot read touched repository: get=%d get_multiple=%d", repo.getValueCalls, repo.getMultipleCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility(t *testing.T) {
|
||||
repo := newRuntimeSettingRepoStub()
|
||||
svc := &OpsService{settingRepo: repo}
|
||||
|
||||
cfg := defaultOpsAdvancedSettings()
|
||||
cfg.DisplayOpenAITokenStats = true
|
||||
cfg.DisplayAlertEvents = false
|
||||
|
||||
updated, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateOpsAdvancedSettings() error = %v", err)
|
||||
}
|
||||
if !updated.DisplayOpenAITokenStats {
|
||||
t.Fatalf("DisplayOpenAITokenStats = false, want true")
|
||||
}
|
||||
if updated.DisplayAlertEvents {
|
||||
t.Fatalf("DisplayAlertEvents = true, want false")
|
||||
}
|
||||
readsAfterUpdate := repo.getValueCalls + repo.getMultipleCalls
|
||||
|
||||
reloaded, err := svc.GetOpsAdvancedSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOpsAdvancedSettings() after update error = %v", err)
|
||||
}
|
||||
if !reloaded.DisplayOpenAITokenStats {
|
||||
t.Fatalf("reloaded DisplayOpenAITokenStats = false, want true")
|
||||
}
|
||||
if reloaded.DisplayAlertEvents {
|
||||
t.Fatalf("reloaded DisplayAlertEvents = true, want false")
|
||||
}
|
||||
if got := repo.getValueCalls + repo.getMultipleCalls; got != readsAfterUpdate {
|
||||
t.Fatalf("snapshot reload performed repository read: before=%d after=%d", readsAfterUpdate, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOpsAdvancedSettings_BackfillsNewDisplayFlagsFromDefaults(t *testing.T) {
|
||||
repo := newRuntimeSettingRepoStub()
|
||||
svc := &OpsService{settingRepo: repo}
|
||||
|
||||
legacyCfg := map[string]any{
|
||||
"data_retention": map[string]any{
|
||||
"cleanup_enabled": false,
|
||||
"cleanup_schedule": "0 2 * * *",
|
||||
"error_log_retention_days": 30,
|
||||
"minute_metrics_retention_days": 30,
|
||||
"hourly_metrics_retention_days": 30,
|
||||
},
|
||||
"aggregation": map[string]any{
|
||||
"aggregation_enabled": false,
|
||||
},
|
||||
"ignore_count_tokens_errors": true,
|
||||
"ignore_context_canceled": true,
|
||||
"ignore_no_available_accounts": false,
|
||||
"ignore_invalid_api_key_errors": true,
|
||||
"auto_refresh_enabled": false,
|
||||
"auto_refresh_interval_seconds": 30,
|
||||
}
|
||||
raw, err := json.Marshal(legacyCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal legacy config: %v", err)
|
||||
}
|
||||
repo.values[SettingKeyOpsAdvancedSettings] = string(raw)
|
||||
|
||||
cfg, err := svc.GetOpsAdvancedSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
|
||||
}
|
||||
if cfg.DisplayOpenAITokenStats {
|
||||
t.Fatalf("DisplayOpenAITokenStats = true, want false default backfill")
|
||||
}
|
||||
if !cfg.DisplayAlertEvents {
|
||||
t.Fatalf("DisplayAlertEvents = false, want true default backfill")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOpenAIQuotaAutoPauseSettings_ReadsDefaultsFromOpsAdvancedSettings(t *testing.T) {
|
||||
repo := newRuntimeSettingRepoStub()
|
||||
repo.values[SettingKeyOpsAdvancedSettings] = `{"openai_account_quota_auto_pause":{"default_threshold_5h":0.95,"default_threshold_7d":0.9}}`
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
// Warm the in-memory cache synchronously so the assertion below is deterministic.
|
||||
// GetOpenAIQuotaAutoPauseSettings is non-blocking on the hot path (returns the
|
||||
// cached value, refreshes asynchronously); for tests and startup, Warm is the
|
||||
// synchronous entry point that guarantees a populated cache.
|
||||
settings := svc.WarmOpenAIQuotaAutoPauseSettings(context.Background())
|
||||
if settings.DefaultThreshold5h != 0.95 {
|
||||
t.Fatalf("DefaultThreshold5h = %v, want 0.95", settings.DefaultThreshold5h)
|
||||
}
|
||||
if settings.DefaultThreshold7d != 0.9 {
|
||||
t.Fatalf("DefaultThreshold7d = %v, want 0.9", settings.DefaultThreshold7d)
|
||||
}
|
||||
|
||||
// Subsequent Get must hit the warm cache and return the same value without any DB
|
||||
// access — that's the hot-path invariant.
|
||||
cached := svc.GetOpenAIQuotaAutoPauseSettings(context.Background())
|
||||
if cached.DefaultThreshold5h != 0.95 || cached.DefaultThreshold7d != 0.9 {
|
||||
t.Fatalf("cached read = %+v, want {0.95, 0.9}", cached)
|
||||
}
|
||||
}
|
||||
|
||||
// Hot-path invariant: a Get with cold cache must return immediately (zero defaults)
|
||||
// rather than blocking on the DB. The async refresher will populate the cache for
|
||||
// subsequent calls.
|
||||
func TestGetOpenAIQuotaAutoPauseSettings_ColdCacheNonBlocking(t *testing.T) {
|
||||
repo := newRuntimeSettingRepoStub()
|
||||
repo.values[SettingKeyOpsAdvancedSettings] = `{"openai_account_quota_auto_pause":{"default_threshold_5h":0.7}}`
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
start := time.Now()
|
||||
settings := svc.GetOpenAIQuotaAutoPauseSettings(context.Background())
|
||||
elapsed := time.Since(start)
|
||||
if elapsed > 50*time.Millisecond {
|
||||
t.Fatalf("cold-cache Get must be non-blocking, took %v", elapsed)
|
||||
}
|
||||
// Cold cache means we get zero defaults (the async refresh hasn't completed yet).
|
||||
if settings.DefaultThreshold5h != 0 || settings.DefaultThreshold7d != 0 {
|
||||
t.Fatalf("cold-cache Get = %+v, want zeroes", settings)
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit cache write (e.g. from UpdateOpsAdvancedSettings) must be visible on the
|
||||
// very next read without any DB roundtrip.
|
||||
func TestSetOpenAIQuotaAutoPauseSettings_VisibleImmediately(t *testing.T) {
|
||||
svc := NewSettingService(newRuntimeSettingRepoStub(), &config.Config{})
|
||||
|
||||
svc.SetOpenAIQuotaAutoPauseSettings(OpsOpenAIAccountQuotaAutoPauseSettings{
|
||||
DefaultThreshold5h: 0.88,
|
||||
DefaultThreshold7d: 0.77,
|
||||
})
|
||||
|
||||
got := svc.GetOpenAIQuotaAutoPauseSettings(context.Background())
|
||||
if got.DefaultThreshold5h != 0.88 || got.DefaultThreshold7d != 0.77 {
|
||||
t.Fatalf("after Set, Get = %+v, want {0.88, 0.77}", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user