astrbbbb / tests /unit /test_config.py
qa1145's picture
Upload 1245 files
8ede856 verified
"""Tests for config module."""
import json
import os
import pytest
from astrbot.core.config.astrbot_config import AstrBotConfig, RateLimitStrategy
from astrbot.core.config.default import DEFAULT_VALUE_MAP
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
@pytest.fixture
def temp_config_path(tmp_path):
"""Create a temporary config path."""
return str(tmp_path / "test_config.json")
@pytest.fixture
def minimal_default_config():
"""Create a minimal default config for testing."""
return {
"config_version": 2,
"platform_settings": {
"unique_session": False,
"rate_limit": {
"time": 60,
"count": 30,
"strategy": "stall",
},
},
"provider_settings": {
"enable": True,
"default_provider_id": "",
},
}
class TestRateLimitStrategy:
"""Tests for RateLimitStrategy enum."""
def test_stall_value(self):
"""Test stall enum value."""
assert RateLimitStrategy.STALL.value == "stall"
def test_discard_value(self):
"""Test discard enum value."""
assert RateLimitStrategy.DISCARD.value == "discard"
class TestAstrBotConfigLoad:
"""Tests for AstrBotConfig loading and initialization."""
def test_init_creates_file_if_not_exists(
self, temp_config_path, minimal_default_config
):
"""Test that config file is created when it doesn't exist."""
assert not os.path.exists(temp_config_path)
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert os.path.exists(temp_config_path)
assert config.config_version == 2
assert config.platform_settings["unique_session"] is False
def test_init_loads_existing_file(self, temp_config_path, minimal_default_config):
"""Test that existing config file is loaded."""
existing_config = {
"config_version": 2,
"platform_settings": {"unique_session": True},
"provider_settings": {"enable": False},
}
with open(temp_config_path, "w", encoding="utf-8-sig") as f:
json.dump(existing_config, f)
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert config.platform_settings["unique_session"] is True
assert config.provider_settings["enable"] is False
def test_first_deploy_flag(self, temp_config_path, minimal_default_config):
"""Test first_deploy flag is set for new config."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert hasattr(config, "first_deploy")
assert config.first_deploy is True
def test_init_with_schema(self, temp_config_path):
"""Test initialization with schema."""
schema = {
"test_field": {
"type": "string",
"default": "test_value",
},
"nested": {
"type": "object",
"items": {
"enabled": {"type": "bool"},
"count": {"type": "int"},
},
},
}
config = AstrBotConfig(config_path=temp_config_path, schema=schema)
assert config.test_field == "test_value"
assert config.nested["enabled"] is False
assert config.nested["count"] == 0
def test_dot_notation_access(self, temp_config_path, minimal_default_config):
"""Test accessing config values using dot notation."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert config.platform_settings is not None
assert config.non_existent_field is None
def test_setattr_updates_config(self, temp_config_path, minimal_default_config):
"""Test that setting attributes updates config."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
config.new_field = "new_value"
assert config.new_field == "new_value"
def test_delattr_removes_field(self, temp_config_path, minimal_default_config):
"""Test that deleting attributes removes them."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
config.temp_field = "temp"
del config.temp_field
# Accessing a deleted field returns None due to __getattr__
assert config.temp_field is None
# But the field is removed from the dict
assert "temp_field" not in config
def test_delattr_saves_config(self, temp_config_path, minimal_default_config):
"""Test that deleting attributes saves config to file."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
config.temp_field = "temp"
del config.temp_field
with open(temp_config_path, encoding="utf-8-sig") as f:
loaded_config = json.load(f)
assert "temp_field" not in loaded_config
def test_check_exist(self, temp_config_path, minimal_default_config):
"""Test check_exist method."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert config.check_exist() is True
# Create a path that definitely doesn't exist
import pathlib
temp_dir = pathlib.Path(temp_config_path).parent
non_existent_path = str(temp_dir / "non_existent_config.json")
# Check that the file doesn't exist before creating config
assert not os.path.exists(non_existent_path)
# Create config which will auto-create the file
config2 = AstrBotConfig(
config_path=non_existent_path, default_config=minimal_default_config
)
# Now it exists
assert config2.check_exist() is True
assert os.path.exists(non_existent_path)
class TestConfigValidation:
"""Tests for config validation and integrity checking."""
def test_insert_missing_config_items(
self, temp_config_path, minimal_default_config
):
"""Test that missing config items are inserted with default values."""
existing_config = {"config_version": 2}
with open(temp_config_path, "w", encoding="utf-8-sig") as f:
json.dump(existing_config, f)
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert "platform_settings" in config
assert "provider_settings" in config
def test_replace_none_with_default(self, temp_config_path, minimal_default_config):
"""Test that None values are replaced with defaults."""
existing_config = {
"config_version": 2,
"platform_settings": None,
"provider_settings": None,
}
with open(temp_config_path, "w", encoding="utf-8-sig") as f:
json.dump(existing_config, f)
AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
# Reload to verify the values were replaced
config2 = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert config2.platform_settings is not None
assert config2.provider_settings is not None
def test_reorder_config_keys(self, temp_config_path, minimal_default_config):
"""Test that config keys are reordered to match default."""
existing_config = {
"provider_settings": {"enable": True},
"config_version": 2,
"platform_settings": {"unique_session": False},
}
with open(temp_config_path, "w", encoding="utf-8-sig") as f:
json.dump(existing_config, f)
AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
with open(temp_config_path, encoding="utf-8-sig") as f:
loaded_config = json.load(f)
keys = list(loaded_config.keys())
assert keys[0] == "config_version"
assert keys[1] == "platform_settings"
assert keys[2] == "provider_settings"
def test_remove_unknown_config_keys(self, temp_config_path, minimal_default_config):
"""Test that unknown config keys are removed."""
existing_config = {
"config_version": 2,
"platform_settings": {},
"unknown_key": "should_be_removed",
}
with open(temp_config_path, "w", encoding="utf-8-sig") as f:
json.dump(existing_config, f)
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert "unknown_key" not in config
def test_nested_config_validation(self, temp_config_path):
"""Test validation of nested config structures."""
default_config = {
"nested": {
"level1": {
"level2": {
"value": 42,
},
},
},
}
existing_config = {
"nested": {
"level1": {}, # Missing level2
},
}
with open(temp_config_path, "w", encoding="utf-8-sig") as f:
json.dump(existing_config, f)
config = AstrBotConfig(
config_path=temp_config_path, default_config=default_config
)
assert "level2" in config.nested["level1"]
assert config.nested["level1"]["level2"]["value"] == 42
class TestConfigHotReload:
"""Tests for config hot reload functionality."""
def test_save_config(self, temp_config_path, minimal_default_config):
"""Test saving config to file."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
config.new_field = "new_value"
config.save_config()
with open(temp_config_path, encoding="utf-8-sig") as f:
loaded_config = json.load(f)
assert loaded_config["new_field"] == "new_value"
def test_save_config_with_replace(self, temp_config_path, minimal_default_config):
"""Test saving config with replacement."""
config = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
replacement_config = {
"replaced": True,
"extra_field": "value",
}
config.save_config(replace_config=replacement_config)
with open(temp_config_path, encoding="utf-8-sig") as f:
loaded_config = json.load(f)
# The replacement config is merged with existing config
assert loaded_config["replaced"] is True
assert loaded_config["extra_field"] == "value"
# Original fields are preserved because update merges
assert "platform_settings" in loaded_config
def test_modification_persists_after_reload(
self, temp_config_path, minimal_default_config
):
"""Test that modifications persist after reloading."""
config1 = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
config1.platform_settings["unique_session"] = True
config1.save_config()
config2 = AstrBotConfig(
config_path=temp_config_path, default_config=minimal_default_config
)
assert config2.platform_settings["unique_session"] is True
class TestConfigSchemaToDefault:
"""Tests for schema to default config conversion."""
def test_convert_schema_with_defaults(self, temp_config_path):
"""Test converting schema with explicit defaults."""
schema = {
"string_field": {"type": "string", "default": "custom"},
"int_field": {"type": "int", "default": 100},
"bool_field": {"type": "bool", "default": True},
}
config = AstrBotConfig(config_path=temp_config_path, schema=schema)
assert config.string_field == "custom"
assert config.int_field == 100
assert config.bool_field is True
def test_convert_schema_without_defaults(self, temp_config_path):
"""Test converting schema using default value map."""
schema = {
"string_field": {"type": "string"},
"int_field": {"type": "int"},
"bool_field": {"type": "bool"},
}
config = AstrBotConfig(config_path=temp_config_path, schema=schema)
assert config.string_field == DEFAULT_VALUE_MAP["string"]
assert config.int_field == DEFAULT_VALUE_MAP["int"]
assert config.bool_field == DEFAULT_VALUE_MAP["bool"]
def test_unsupported_schema_type_raises_error(self, temp_config_path):
"""Test that unsupported schema types raise error."""
schema = {
"field": {"type": "unsupported_type"},
}
with pytest.raises(TypeError, match="不受支持的配置类型"):
AstrBotConfig(config_path=temp_config_path, schema=schema)
def test_template_list_type(self, temp_config_path):
"""Test template_list schema type."""
schema = {
"templates": {"type": "template_list", "default": []},
}
config = AstrBotConfig(config_path=temp_config_path, schema=schema)
assert config.templates == []
def test_nested_object_schema(self, temp_config_path):
"""Test nested object schema conversion."""
schema = {
"nested": {
"type": "object",
"items": {
"field1": {"type": "string"},
"field2": {"type": "int"},
},
},
}
config = AstrBotConfig(config_path=temp_config_path, schema=schema)
assert config.nested["field1"] == ""
assert config.nested["field2"] == 0
class TestConfigMetadataI18n:
"""Tests for i18n utils."""
def test_get_i18n_key(self):
"""Test generating i18n key."""
key = ConfigMetadataI18n._get_i18n_key(
group="ai_group",
section="general",
field="enable",
attr="description",
)
assert key == "ai_group.general.enable.description"
def test_get_i18n_key_without_field(self):
"""Test generating i18n key without field."""
key = ConfigMetadataI18n._get_i18n_key(
group="ai_group",
section="general",
field="",
attr="description",
)
assert key == "ai_group.general.description"
def test_convert_to_i18n_keys_simple(self):
"""Test converting simple metadata to i18n keys."""
metadata = {
"ai_group": {
"name": "AI Settings",
"metadata": {
"general": {
"description": "General settings",
"items": {
"enable": {
"description": "Enable feature",
"type": "bool",
"default": True,
},
},
},
},
},
}
result = ConfigMetadataI18n.convert_to_i18n_keys(metadata)
assert result["ai_group"]["name"] == "ai_group.name"
assert (
result["ai_group"]["metadata"]["general"]["description"]
== "ai_group.general.description"
)
assert (
result["ai_group"]["metadata"]["general"]["items"]["enable"]["description"]
== "ai_group.general.enable.description"
)
def test_convert_to_i18n_keys_with_hint(self):
"""Test converting metadata with hint."""
metadata = {
"group": {
"metadata": {
"section": {
"hint": "This is a hint",
"items": {
"field": {
"hint": "Field hint",
"type": "string",
},
},
},
},
},
}
result = ConfigMetadataI18n.convert_to_i18n_keys(metadata)
assert result["group"]["metadata"]["section"]["hint"] == "group.section.hint"
assert (
result["group"]["metadata"]["section"]["items"]["field"]["hint"]
== "group.section.field.hint"
)
def test_convert_to_i18n_keys_with_labels(self):
"""Test converting metadata with labels."""
metadata = {
"group": {
"metadata": {
"section": {
"items": {
"field": {
"labels": ["Label1", "Label2"],
"type": "string",
},
},
},
},
},
}
result = ConfigMetadataI18n.convert_to_i18n_keys(metadata)
assert (
result["group"]["metadata"]["section"]["items"]["field"]["labels"]
== "group.section.field.labels"
)
def test_convert_to_i18n_keys_nested_items(self):
"""Test converting metadata with nested items."""
metadata = {
"group": {
"metadata": {
"section": {
"items": {
"nested": {
"description": "Nested field",
"type": "object",
"items": {
"inner": {
"description": "Inner field",
"type": "string",
},
},
},
},
},
},
},
}
result = ConfigMetadataI18n.convert_to_i18n_keys(metadata)
assert (
result["group"]["metadata"]["section"]["items"]["nested"]["description"]
== "group.section.nested.description"
)
assert (
result["group"]["metadata"]["section"]["items"]["nested"]["items"]["inner"][
"description"
]
== "group.section.nested.inner.description"
)
def test_convert_to_i18n_keys_preserves_non_i18n_fields(self):
"""Test that non-i18n fields are preserved."""
metadata = {
"group": {
"metadata": {
"section": {
"items": {
"field": {
"description": "Field description",
"type": "string",
"other_field": "preserve this",
},
},
},
},
},
}
result = ConfigMetadataI18n.convert_to_i18n_keys(metadata)
assert (
result["group"]["metadata"]["section"]["items"]["field"]["other_field"]
== "preserve this"
)
def test_convert_to_i18n_keys_with_name(self):
"""Test converting metadata with name field."""
metadata = {
"group": {
"metadata": {
"section": {
"items": {
"field": {
"name": "Field Name",
"type": "string",
},
},
},
},
},
}
result = ConfigMetadataI18n.convert_to_i18n_keys(metadata)
assert (
result["group"]["metadata"]["section"]["items"]["field"]["name"]
== "group.section.field.name"
)