| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- //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)")
- })
- }
|