| """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 |
|
|
| |
| assert config.temp_field is None |
| |
| 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 |
|
|
| |
| import pathlib |
|
|
| temp_dir = pathlib.Path(temp_config_path).parent |
| non_existent_path = str(temp_dir / "non_existent_config.json") |
|
|
| |
| assert not os.path.exists(non_existent_path) |
|
|
| |
| config2 = AstrBotConfig( |
| config_path=non_existent_path, default_config=minimal_default_config |
| ) |
|
|
| |
| 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 |
| ) |
|
|
| |
| 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": {}, |
| }, |
| } |
| 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) |
|
|
| |
| assert loaded_config["replaced"] is True |
| assert loaded_config["extra_field"] == "value" |
| |
| 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" |
| ) |
|
|