2026-02-27 13:58:58 +08:00
|
|
|
"""Tests for pool_config.py — configuration parsing and defaults."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from src.services.provider.pool.config import (
|
|
|
|
|
PoolConfig,
|
2026-03-04 22:06:19 +08:00
|
|
|
SchedulingPreset,
|
|
|
|
|
ScoringWeights,
|
2026-02-27 13:58:58 +08:00
|
|
|
UnschedulableRule,
|
|
|
|
|
parse_pool_config,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_returns_none_when_no_advanced_section() -> None:
|
|
|
|
|
assert parse_pool_config({}) is None
|
|
|
|
|
assert parse_pool_config(None) is None
|
|
|
|
|
assert parse_pool_config({"other_key": 1}) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_returns_defaults_for_empty_advanced() -> None:
|
|
|
|
|
cfg = parse_pool_config({"pool_advanced": {}})
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.sticky_session_ttl_seconds == 3600
|
|
|
|
|
assert cfg.load_threshold_percent == 80
|
2026-03-10 10:40:50 +08:00
|
|
|
assert cfg.lru_enabled is False
|
|
|
|
|
assert cfg.scheduling_mode == "multi_score"
|
2026-03-04 22:06:19 +08:00
|
|
|
assert cfg.scoring_weights == ScoringWeights()
|
2026-03-10 10:40:50 +08:00
|
|
|
# Default: only cache_affinity preset enabled
|
2026-03-04 22:06:19 +08:00
|
|
|
assert len(cfg.scheduling_presets) == 1
|
2026-03-10 10:40:50 +08:00
|
|
|
assert cfg.scheduling_presets[0].preset == "cache_affinity"
|
2026-03-04 22:06:19 +08:00
|
|
|
assert cfg.scheduling_presets[0].enabled is True
|
|
|
|
|
assert cfg.latency_window_seconds == 3600
|
|
|
|
|
assert cfg.latency_sample_limit == 50
|
2026-02-27 13:58:58 +08:00
|
|
|
assert cfg.cost_window_seconds == 18000
|
|
|
|
|
assert cfg.cost_limit_per_key_tokens is None
|
|
|
|
|
assert cfg.cost_soft_threshold_percent == 80
|
|
|
|
|
assert cfg.rate_limit_cooldown_seconds == 300
|
|
|
|
|
assert cfg.overload_cooldown_seconds == 30
|
|
|
|
|
assert cfg.proactive_refresh_seconds == 180
|
|
|
|
|
assert cfg.health_policy_enabled is True
|
|
|
|
|
assert cfg.unschedulable_rules == []
|
2026-03-06 13:13:01 +08:00
|
|
|
assert cfg.batch_concurrency == 8
|
2026-03-05 15:15:26 +08:00
|
|
|
assert cfg.probing_enabled is False
|
|
|
|
|
assert cfg.probing_interval_minutes == 10
|
|
|
|
|
assert cfg.auto_remove_banned_keys is False
|
2026-02-27 13:58:58 +08:00
|
|
|
|
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
def test_parse_pool_config_overrides_values_legacy_string_list() -> None:
|
|
|
|
|
"""Legacy string-list format with scheduling_mode/lru_enabled."""
|
2026-02-27 13:58:58 +08:00
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"sticky_session_ttl_seconds": 7200,
|
|
|
|
|
"load_threshold_percent": 90,
|
|
|
|
|
"lru_enabled": False,
|
2026-03-04 22:06:19 +08:00
|
|
|
"scheduling_mode": "multi_score",
|
|
|
|
|
"scheduling_presets": ["free_team_first", "recent_refresh", "free_team_first"],
|
|
|
|
|
"scoring_weights": {
|
|
|
|
|
"lru": 0.1,
|
|
|
|
|
"latency": 0.5,
|
|
|
|
|
"health": 0.2,
|
|
|
|
|
"cost_remaining": 0.2,
|
|
|
|
|
},
|
|
|
|
|
"latency_window_seconds": 7200,
|
|
|
|
|
"latency_sample_limit": 80,
|
2026-02-27 13:58:58 +08:00
|
|
|
"cost_window_seconds": 36000,
|
|
|
|
|
"cost_limit_per_key_tokens": 100000,
|
|
|
|
|
"cost_soft_threshold_percent": 70,
|
|
|
|
|
"rate_limit_cooldown_seconds": 600,
|
|
|
|
|
"overload_cooldown_seconds": 60,
|
|
|
|
|
"proactive_refresh_seconds": 300,
|
|
|
|
|
"health_policy_enabled": False,
|
2026-03-05 15:15:26 +08:00
|
|
|
"probing_enabled": True,
|
|
|
|
|
"probing_interval_minutes": 15,
|
|
|
|
|
"auto_remove_banned_keys": True,
|
2026-02-27 13:58:58 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.sticky_session_ttl_seconds == 7200
|
|
|
|
|
assert cfg.load_threshold_percent == 90
|
|
|
|
|
assert cfg.lru_enabled is False
|
2026-03-04 22:06:19 +08:00
|
|
|
assert cfg.scheduling_mode == "multi_score"
|
|
|
|
|
# Legacy string list → SchedulingPreset objects, deduped
|
|
|
|
|
preset_names = tuple(p.preset for p in cfg.scheduling_presets)
|
|
|
|
|
assert "free_team_first" in preset_names
|
|
|
|
|
assert "recent_refresh" in preset_names
|
|
|
|
|
assert cfg.scoring_weights == ScoringWeights(
|
|
|
|
|
lru=0.1,
|
|
|
|
|
latency=0.5,
|
|
|
|
|
health=0.2,
|
|
|
|
|
cost_remaining=0.2,
|
|
|
|
|
)
|
|
|
|
|
assert cfg.latency_window_seconds == 7200
|
|
|
|
|
assert cfg.latency_sample_limit == 80
|
|
|
|
|
assert "multi_score" in cfg.strategies
|
2026-02-27 13:58:58 +08:00
|
|
|
assert cfg.cost_window_seconds == 36000
|
|
|
|
|
assert cfg.cost_limit_per_key_tokens == 100000
|
|
|
|
|
assert cfg.cost_soft_threshold_percent == 70
|
|
|
|
|
assert cfg.rate_limit_cooldown_seconds == 600
|
|
|
|
|
assert cfg.overload_cooldown_seconds == 60
|
|
|
|
|
assert cfg.proactive_refresh_seconds == 300
|
|
|
|
|
assert cfg.health_policy_enabled is False
|
2026-03-06 13:13:01 +08:00
|
|
|
assert cfg.batch_concurrency == 8
|
2026-03-05 15:15:26 +08:00
|
|
|
assert cfg.probing_enabled is True
|
|
|
|
|
assert cfg.probing_interval_minutes == 15
|
|
|
|
|
assert cfg.auto_remove_banned_keys is True
|
2026-02-27 13:58:58 +08:00
|
|
|
|
|
|
|
|
|
2026-03-06 13:13:01 +08:00
|
|
|
def test_parse_pool_config_parses_batch_concurrency() -> None:
|
|
|
|
|
cfg = parse_pool_config({"pool_advanced": {"batch_concurrency": 12}})
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.batch_concurrency == 12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_clamps_batch_concurrency() -> None:
|
|
|
|
|
cfg = parse_pool_config({"pool_advanced": {"batch_concurrency": 99}})
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.batch_concurrency == 32
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
def test_parse_pool_config_new_object_list_format() -> None:
|
|
|
|
|
"""New object-list format: [{preset, enabled, mode}]."""
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "lru", "enabled": True},
|
|
|
|
|
{"preset": "free_team_first", "enabled": True, "mode": "free_only"},
|
|
|
|
|
{"preset": "quota_balanced", "enabled": False},
|
|
|
|
|
{"preset": "recent_refresh", "enabled": True},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert len(cfg.scheduling_presets) == 4
|
|
|
|
|
|
|
|
|
|
lru = cfg.scheduling_presets[0]
|
|
|
|
|
assert lru.preset == "lru"
|
|
|
|
|
assert lru.enabled is True
|
|
|
|
|
|
|
|
|
|
ftf = cfg.scheduling_presets[1]
|
|
|
|
|
assert ftf.preset == "free_team_first"
|
|
|
|
|
assert ftf.enabled is True
|
|
|
|
|
assert ftf.mode == "free_only"
|
|
|
|
|
|
|
|
|
|
qb = cfg.scheduling_presets[2]
|
|
|
|
|
assert qb.preset == "quota_balanced"
|
|
|
|
|
assert qb.enabled is False
|
|
|
|
|
|
|
|
|
|
rr = cfg.scheduling_presets[3]
|
|
|
|
|
assert rr.preset == "recent_refresh"
|
|
|
|
|
assert rr.enabled is True
|
|
|
|
|
|
|
|
|
|
# Derived fields: lru enabled, non-lru enabled → multi_score
|
|
|
|
|
assert cfg.lru_enabled is True
|
|
|
|
|
assert cfg.scheduling_mode == "multi_score"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_new_format_lru_only() -> None:
|
|
|
|
|
"""When only LRU is enabled, scheduling_mode should be 'lru'."""
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "lru", "enabled": True},
|
|
|
|
|
{"preset": "quota_balanced", "enabled": False},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.lru_enabled is True
|
|
|
|
|
assert cfg.scheduling_mode == "lru"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_new_format_lru_disabled() -> None:
|
|
|
|
|
"""LRU disabled, other presets enabled → multi_score + lru_enabled=False."""
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "lru", "enabled": False},
|
|
|
|
|
{"preset": "quota_balanced", "enabled": True},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.lru_enabled is False
|
|
|
|
|
assert cfg.scheduling_mode == "multi_score"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_new_format_free_team_mode_validation() -> None:
|
|
|
|
|
"""Invalid mode falls back to 'both'."""
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "free_team_first", "enabled": True, "mode": "invalid_mode"},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
ftf = [p for p in cfg.scheduling_presets if p.preset == "free_team_first"][0]
|
|
|
|
|
assert ftf.mode == "both"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_new_format_dedup_presets() -> None:
|
|
|
|
|
"""Duplicate presets in object list should be deduplicated."""
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "lru", "enabled": True},
|
|
|
|
|
{"preset": "lru", "enabled": False},
|
|
|
|
|
{"preset": "quota_balanced", "enabled": True},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
lru_presets = [p for p in cfg.scheduling_presets if p.preset == "lru"]
|
|
|
|
|
assert len(lru_presets) == 1
|
|
|
|
|
assert lru_presets[0].enabled is True # first occurrence wins
|
|
|
|
|
|
|
|
|
|
|
2026-02-27 13:58:58 +08:00
|
|
|
def test_parse_pool_config_parses_unschedulable_rules() -> None:
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"unschedulable_rules": [
|
|
|
|
|
{"keyword": "rate_limit", "duration_minutes": 10},
|
|
|
|
|
{"keyword": "overloaded"},
|
|
|
|
|
{"invalid": "entry"}, # should be skipped
|
|
|
|
|
"not_a_dict", # should be skipped
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert len(cfg.unschedulable_rules) == 2
|
|
|
|
|
assert cfg.unschedulable_rules[0] == UnschedulableRule(
|
|
|
|
|
keyword="rate_limit", duration_minutes=10
|
|
|
|
|
)
|
|
|
|
|
assert cfg.unschedulable_rules[1] == UnschedulableRule(keyword="overloaded", duration_minutes=5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_handles_invalid_types_gracefully() -> None:
|
|
|
|
|
# Invalid int values should fall back to defaults
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"sticky_session_ttl_seconds": "not_a_number",
|
|
|
|
|
"cost_limit_per_key_tokens": "bad",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.sticky_session_ttl_seconds == 3600 # default
|
|
|
|
|
assert cfg.cost_limit_per_key_tokens is None # default for opt_int
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 10:40:50 +08:00
|
|
|
def test_parse_pool_config_invalid_scheduling_mode_falls_back_to_cache_affinity() -> None:
|
2026-03-04 22:06:19 +08:00
|
|
|
cfg = parse_pool_config({"pool_advanced": {"scheduling_mode": "unknown"}})
|
|
|
|
|
assert cfg is not None
|
2026-03-10 10:40:50 +08:00
|
|
|
# Default with no explicit presets: cache_affinity -> multi_score
|
|
|
|
|
assert cfg.scheduling_mode == "multi_score"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_scoring_weights_invalid_values_are_clamped() -> None:
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_mode": "multi_score",
|
|
|
|
|
"scoring_weights": {
|
|
|
|
|
"lru": 2.0,
|
|
|
|
|
"latency": -1.0,
|
|
|
|
|
"health": "bad",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
assert cfg.scoring_weights.lru == 1.0
|
|
|
|
|
assert cfg.scoring_weights.latency == 0.0
|
|
|
|
|
assert cfg.scoring_weights.health == 0.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_pool_config_invalid_scheduling_presets_are_ignored() -> None:
|
|
|
|
|
cfg = parse_pool_config(
|
|
|
|
|
{
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_mode": "multi_score",
|
|
|
|
|
"scheduling_presets": ["quota_balanced", "unknown", 123, "single_account"],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
preset_names = tuple(p.preset for p in cfg.scheduling_presets if p.preset != "lru")
|
|
|
|
|
assert "quota_balanced" in preset_names
|
|
|
|
|
assert "single_account" in preset_names
|
|
|
|
|
assert "unknown" not in preset_names
|
|
|
|
|
|
|
|
|
|
|
2026-02-27 13:58:58 +08:00
|
|
|
def test_pool_config_is_frozen() -> None:
|
|
|
|
|
cfg = PoolConfig()
|
|
|
|
|
try:
|
|
|
|
|
cfg.sticky_session_ttl_seconds = 999 # type: ignore[misc]
|
|
|
|
|
assert False, "Should have raised FrozenInstanceError"
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|
2026-03-04 22:06:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_scheduling_preset_is_frozen() -> None:
|
|
|
|
|
preset = SchedulingPreset(preset="lru", enabled=True)
|
|
|
|
|
try:
|
|
|
|
|
preset.enabled = False # type: ignore[misc]
|
|
|
|
|
assert False, "Should have raised FrozenInstanceError"
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|