Files
sub2api/backend/internal/service/openai_spark_shadow_parent_health_test.go
T
李建琦 6d655c9903
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
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

221 lines
7.3 KiB
Go

//go:build unit
package service
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestParentHealthyForShadow covers the pure helper function used across
// scheduler + gateway selection + WS forwarder.
func TestParentHealthyForShadow(t *testing.T) {
pid := int64(100)
// 所有母账号 fixture 均设 Type=oauth:parentHealthyForShadow 现要求母账号仍是 OpenAI OAuth
// (外审 D fail-closed),不设则各用例会因"非 oauth"而非被测原因失败,使断言失去意义。
healthyParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
}
unhealthyParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusError,
Schedulable: true, // Schedulable flag is set, but Status=error → IsSchedulable()==false
}
shadow := &Account{
ID: 200,
ParentAccountID: &pid,
QuotaDimension: QuotaDimensionSpark,
Platform: PlatformOpenAI,
Status: StatusActive,
Schedulable: true,
}
normalAccount := &Account{
ID: 300,
Platform: PlatformOpenAI,
Status: StatusActive,
Schedulable: true,
}
t.Run("shadow_of_healthy_parent_is_healthy", func(t *testing.T) {
lookup := func(id int64) *Account {
if id == healthyParent.ID {
return healthyParent
}
return nil
}
require.True(t, parentHealthyForShadow(shadow, lookup))
})
t.Run("shadow_of_unhealthy_parent_is_not_healthy", func(t *testing.T) {
// Parent Status=error means IsActive()==false → IsSchedulable()==false.
require.False(t, unhealthyParent.IsSchedulable(), "precondition: unhealthy parent must not be schedulable")
lookup := func(id int64) *Account {
if id == unhealthyParent.ID {
return unhealthyParent
}
return nil
}
require.False(t, parentHealthyForShadow(shadow, lookup))
})
t.Run("shadow_parent_not_found_is_not_healthy", func(t *testing.T) {
lookup := func(_ int64) *Account { return nil }
require.False(t, parentHealthyForShadow(shadow, lookup))
})
t.Run("normal_account_always_healthy", func(t *testing.T) {
// lookup should never be called for non-shadow accounts.
calledLookup := false
lookup := func(_ int64) *Account {
calledLookup = true
return nil
}
require.True(t, parentHealthyForShadow(normalAccount, lookup))
require.False(t, calledLookup, "lookup must not be called for non-shadow accounts")
})
t.Run("nil_account_always_healthy", func(t *testing.T) {
lookup := func(_ int64) *Account { return nil }
require.True(t, parentHealthyForShadow(nil, lookup))
})
t.Run("manual_schedulable_false_parent_does_not_block_shadow", func(t *testing.T) {
// F1 决策 A:母账号被手动暂停(Schedulable=false)是「调度配置」而非「凭据不可用」,
// 不传播到影子——影子有自己的 Schedulable 开关。凭据(active+未过期)仍可用 → 影子健康。
manualPausedParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: false,
}
require.False(t, manualPausedParent.IsSchedulable(), "precondition: 母账号被手动暂停不可调度")
lookup := func(id int64) *Account {
if id == manualPausedParent.ID {
return manualPausedParent
}
return nil
}
require.True(t, parentHealthyForShadow(shadow, lookup),
"母账号手动暂停不应连坐影子(凭据仍可用)")
})
t.Run("global_rate_limited_parent_does_not_block_shadow", func(t *testing.T) {
// F1 核心修复:母账号 global 429(RateLimitResetAt 未来)是 global 维度限流,
// spark 有独立窗口 → 不得连坐影子,否则违背「global 枯竭后 spark 仍独立」目标。
resetAt := time.Now().Add(1 * time.Hour)
rateLimitedParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
RateLimitResetAt: &resetAt,
}
require.False(t, rateLimitedParent.IsSchedulable(), "precondition: global 限流母账号自身不可调度")
lookup := func(id int64) *Account {
if id == rateLimitedParent.ID {
return rateLimitedParent
}
return nil
}
require.True(t, parentHealthyForShadow(shadow, lookup),
"母账号 global 限流不应连坐 spark 影子")
})
t.Run("overloaded_parent_does_not_block_shadow", func(t *testing.T) {
// 过载退避(OverloadUntil)同属 global 维度运行态,不连坐影子。
until := time.Now().Add(30 * time.Minute)
overloadedParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
OverloadUntil: &until,
}
require.False(t, overloadedParent.IsSchedulable(), "precondition: 过载母账号自身不可调度")
lookup := func(id int64) *Account {
if id == overloadedParent.ID {
return overloadedParent
}
return nil
}
require.True(t, parentHealthyForShadow(shadow, lookup),
"母账号过载退避不应连坐 spark 影子")
})
t.Run("temp_unschedulable_parent_blocks_shadow", func(t *testing.T) {
// G2:TempUnschedulableUntil 对 OpenAI 账号由 401/token 刷新耗尽/transport·proxy 写入,
// 代表共享凭据/传输坏死 → 影子共享母 token+proxy,应被挡(与 global 限流 RateLimitResetAt 区分)。
until := time.Now().Add(15 * time.Minute)
tempUnschedParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
TempUnschedulableUntil: &until,
}
lookup := func(id int64) *Account {
if id == tempUnschedParent.ID {
return tempUnschedParent
}
return nil
}
require.False(t, parentHealthyForShadow(shadow, lookup),
"母账号 TempUnschedulableUntil(凭据/传输坏死)冷却期内应挡住影子")
})
t.Run("expired_parent_credentials_block_shadow", func(t *testing.T) {
// 凭据真正过期(AutoPauseOnExpired + ExpiresAt 已过)→ 透传 token 不可用 → 影子应被挡。
expiredAt := time.Now().Add(-1 * time.Hour)
expiredParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
AutoPauseOnExpired: true,
ExpiresAt: &expiredAt,
}
lookup := func(id int64) *Account {
if id == expiredParent.ID {
return expiredParent
}
return nil
}
require.False(t, parentHealthyForShadow(shadow, lookup),
"母账号凭据过期时影子应被挡(透传 token 不可用)")
})
t.Run("non_oauth_parent_blocks_shadow", func(t *testing.T) {
// 外审 D:母账号被改成非 OpenAI OAuth(如 apikey)后,透传凭据解析必失败,
// 影子应 fail-closed 不进调度候选(即便账号 active、凭据未过期)。
apikeyParent := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: true,
}
lookup := func(id int64) *Account {
if id == apikeyParent.ID {
return apikeyParent
}
return nil
}
require.False(t, parentHealthyForShadow(shadow, lookup),
"母账号非 OpenAI OAuth 时影子应被挡(fail-closed)")
})
}