- Added support for new configuration sections: relationship, database, maisaka, mcp, and plugin_runtime. - Introduced complex field hooks for handling JSON configurations in chat talk value rules, expression learning lists, and more. - Updated the field hooks to include schema metadata for better UI representation. - Refactored the bot configuration page to utilize a more dynamic approach for managing section values and state. - Improved the configuration schema generation to ensure all top-level sections have UI metadata. - Added tests to validate the new configuration schema and ensure proper functionality of the JSON field hooks.
109 lines
4.5 KiB
Python
109 lines
4.5 KiB
Python
from src.config.official_configs import ChatConfig, MessageReceiveConfig
|
||
from src.config.config import Config
|
||
from src.webui.config_schema import ConfigSchemaGenerator
|
||
|
||
|
||
def test_field_docs_in_schema():
|
||
"""Test that field descriptions are correctly extracted from field_docs (docstrings)."""
|
||
schema = ConfigSchemaGenerator.generate_schema(ChatConfig)
|
||
talk_value = next(f for f in schema["fields"] if f["name"] == "talk_value")
|
||
|
||
# Verify description field exists
|
||
assert "description" in talk_value
|
||
# Verify description contains expected Chinese text from the docstring
|
||
assert "聊天频率" in talk_value["description"]
|
||
|
||
|
||
def test_json_schema_extra_merged():
|
||
"""Test that json_schema_extra fields are correctly merged into output."""
|
||
schema = ConfigSchemaGenerator.generate_schema(ChatConfig)
|
||
talk_value = next(f for f in schema["fields"] if f["name"] == "talk_value")
|
||
|
||
# Verify UI metadata fields from json_schema_extra exist
|
||
assert talk_value.get("x-widget") == "slider"
|
||
assert talk_value.get("x-icon") == "message-circle"
|
||
assert talk_value.get("step") == 0.1
|
||
|
||
|
||
def test_pydantic_constraints_mapped():
|
||
"""Test that Pydantic constraints (ge/le) are correctly mapped to minValue/maxValue."""
|
||
schema = ConfigSchemaGenerator.generate_schema(ChatConfig)
|
||
talk_value = next(f for f in schema["fields"] if f["name"] == "talk_value")
|
||
|
||
# Verify constraints are mapped to frontend naming convention
|
||
assert "minValue" in talk_value
|
||
assert "maxValue" in talk_value
|
||
assert talk_value["minValue"] == 0 # From ge=0
|
||
assert talk_value["maxValue"] == 1 # From le=1
|
||
|
||
|
||
def test_nested_model_schema():
|
||
"""Test that nested models (ConfigBase fields) are correctly handled."""
|
||
schema = ConfigSchemaGenerator.generate_schema(Config)
|
||
|
||
# Verify nested structure exists
|
||
assert "nested" in schema
|
||
assert "chat" in schema["nested"]
|
||
|
||
# Verify nested chat schema is complete
|
||
chat_schema = schema["nested"]["chat"]
|
||
assert chat_schema["className"] == "ChatConfig"
|
||
assert "fields" in chat_schema
|
||
|
||
# Verify nested schema fields include description and metadata
|
||
talk_value = next(f for f in chat_schema["fields"] if f["name"] == "talk_value")
|
||
assert "description" in talk_value
|
||
assert talk_value.get("x-widget") == "slider"
|
||
assert talk_value.get("minValue") == 0
|
||
|
||
|
||
def test_field_without_extra_metadata():
|
||
"""Test that fields without json_schema_extra still generate valid schema."""
|
||
schema = ConfigSchemaGenerator.generate_schema(ChatConfig)
|
||
inevitable_at_reply = next(f for f in schema["fields"] if f["name"] == "inevitable_at_reply")
|
||
|
||
# Verify basic fields are generated
|
||
assert "name" in inevitable_at_reply
|
||
assert inevitable_at_reply["name"] == "inevitable_at_reply"
|
||
assert "type" in inevitable_at_reply
|
||
assert inevitable_at_reply["type"] == "boolean"
|
||
assert "label" in inevitable_at_reply
|
||
assert "required" in inevitable_at_reply
|
||
|
||
# Verify no x-widget or x-icon from json_schema_extra (since field has none)
|
||
# These fields should only be present if explicitly defined in json_schema_extra
|
||
assert not inevitable_at_reply.get("x-widget")
|
||
assert not inevitable_at_reply.get("x-icon")
|
||
|
||
|
||
def test_all_top_level_sections_have_ui_metadata():
|
||
"""所有顶层配置节都必须声明 uiParent 或独立 Tab 的标签与图标。"""
|
||
schema = ConfigSchemaGenerator.generate_schema(Config)
|
||
|
||
for section_name, section_schema in schema["nested"].items():
|
||
has_parent = bool(section_schema.get("uiParent"))
|
||
has_host_meta = bool(section_schema.get("uiLabel")) and bool(section_schema.get("uiIcon"))
|
||
assert has_parent or has_host_meta, f"{section_name} 缺少 UI 元数据"
|
||
|
||
|
||
def test_maisaka_is_host_tab_and_mcp_is_attached_to_it():
|
||
"""MaiSaka 应作为独立 Tab,MCP 作为其子配置挂载。"""
|
||
schema = ConfigSchemaGenerator.generate_schema(Config)
|
||
|
||
maisaka_schema = schema["nested"]["maisaka"]
|
||
mcp_schema = schema["nested"]["mcp"]
|
||
|
||
assert maisaka_schema.get("uiParent") is None
|
||
assert maisaka_schema.get("uiLabel") == "MaiSaka"
|
||
assert maisaka_schema.get("uiIcon") == "message-circle"
|
||
assert mcp_schema.get("uiParent") == "maisaka"
|
||
|
||
|
||
def test_set_field_is_mapped_as_array():
|
||
"""set[str] 应映射为前端可识别的 array。"""
|
||
schema = ConfigSchemaGenerator.generate_schema(MessageReceiveConfig)
|
||
ban_words = next(field for field in schema["fields"] if field["name"] == "ban_words")
|
||
|
||
assert ban_words["type"] == "array"
|
||
assert ban_words["items"]["type"] == "string"
|