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
221 lines
7.3 KiB
Go
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)")
|
|
})
|
|
}
|