mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
419 lines
16 KiB
Python
419 lines
16 KiB
Python
"""
|
|
Tests for protocol/descriptors.py - Descriptor models.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from astrbot_sdk.protocol.descriptors import (
|
|
BUILTIN_CAPABILITY_SCHEMAS,
|
|
CapabilityDescriptor,
|
|
CommandTrigger,
|
|
DB_LIST_INPUT_SCHEMA,
|
|
EventTrigger,
|
|
HandlerDescriptor,
|
|
LLM_CHAT_INPUT_SCHEMA,
|
|
MessageTrigger,
|
|
Permissions,
|
|
RESERVED_CAPABILITY_PREFIXES,
|
|
SESSION_REF_SCHEMA,
|
|
ScheduleTrigger,
|
|
SessionRef,
|
|
)
|
|
|
|
|
|
class TestPermissions:
|
|
"""Tests for Permissions model."""
|
|
|
|
def test_default_values(self):
|
|
"""Permissions should have default values."""
|
|
perms = Permissions()
|
|
assert perms.require_admin is False
|
|
assert perms.level == 0
|
|
|
|
def test_custom_values(self):
|
|
"""Permissions should accept custom values."""
|
|
perms = Permissions(require_admin=True, level=5)
|
|
assert perms.require_admin is True
|
|
assert perms.level == 5
|
|
|
|
def test_model_dump(self):
|
|
"""Permissions should serialize correctly."""
|
|
perms = Permissions(require_admin=True, level=10)
|
|
data = perms.model_dump()
|
|
assert data == {"require_admin": True, "level": 10}
|
|
|
|
def test_extra_fields_forbidden(self):
|
|
"""Permissions should forbid extra fields."""
|
|
with pytest.raises(ValidationError):
|
|
Permissions(require_admin=True, unknown_field="value")
|
|
|
|
|
|
class TestCommandTrigger:
|
|
"""Tests for CommandTrigger model."""
|
|
|
|
def test_required_command(self):
|
|
"""CommandTrigger requires command field."""
|
|
trigger = CommandTrigger(command="hello")
|
|
assert trigger.type == "command"
|
|
assert trigger.command == "hello"
|
|
assert trigger.aliases == []
|
|
assert trigger.description is None
|
|
assert trigger.platforms == []
|
|
assert trigger.message_types == []
|
|
|
|
def test_with_aliases_and_description(self):
|
|
"""CommandTrigger should accept aliases and description."""
|
|
trigger = CommandTrigger(
|
|
command="hello",
|
|
aliases=["hi", "hey"],
|
|
description="Say hello",
|
|
)
|
|
assert trigger.command == "hello"
|
|
assert trigger.aliases == ["hi", "hey"]
|
|
assert trigger.description == "Say hello"
|
|
|
|
def test_type_literal(self):
|
|
"""CommandTrigger type should always be 'command'."""
|
|
trigger = CommandTrigger(command="test")
|
|
assert trigger.type == "command"
|
|
|
|
def test_extra_fields_forbidden(self):
|
|
"""CommandTrigger should forbid extra fields."""
|
|
with pytest.raises(ValidationError):
|
|
CommandTrigger(command="test", extra="field")
|
|
|
|
|
|
class TestMessageTrigger:
|
|
"""Tests for MessageTrigger model."""
|
|
|
|
def test_default_values(self):
|
|
"""MessageTrigger should have default values."""
|
|
trigger = MessageTrigger()
|
|
assert trigger.type == "message"
|
|
assert trigger.regex is None
|
|
assert trigger.keywords == []
|
|
assert trigger.platforms == []
|
|
assert trigger.message_types == []
|
|
|
|
def test_with_regex(self):
|
|
"""MessageTrigger should accept regex pattern."""
|
|
trigger = MessageTrigger(regex=r"^hello.*$")
|
|
assert trigger.regex == r"^hello.*$"
|
|
|
|
def test_with_keywords(self):
|
|
"""MessageTrigger should accept keywords."""
|
|
trigger = MessageTrigger(keywords=["hello", "hi"])
|
|
assert trigger.keywords == ["hello", "hi"]
|
|
|
|
def test_with_platforms(self):
|
|
"""MessageTrigger should accept platforms."""
|
|
trigger = MessageTrigger(platforms=["wechat", "qq"])
|
|
assert trigger.platforms == ["wechat", "qq"]
|
|
|
|
def test_with_all_fields(self):
|
|
"""MessageTrigger should accept all fields."""
|
|
trigger = MessageTrigger(
|
|
regex=r"test",
|
|
keywords=["keyword"],
|
|
platforms=["platform"],
|
|
message_types=["private"],
|
|
)
|
|
assert trigger.regex == "test"
|
|
assert trigger.keywords == ["keyword"]
|
|
assert trigger.platforms == ["platform"]
|
|
assert trigger.message_types == ["private"]
|
|
|
|
|
|
class TestEventTrigger:
|
|
"""Tests for EventTrigger model."""
|
|
|
|
def test_required_event_type(self):
|
|
"""EventTrigger requires event_type field."""
|
|
trigger = EventTrigger(event_type="message")
|
|
assert trigger.type == "event"
|
|
assert trigger.event_type == "message"
|
|
|
|
def test_numeric_event_type(self):
|
|
"""EventTrigger should accept numeric string event_type."""
|
|
trigger = EventTrigger(event_type="3")
|
|
assert trigger.event_type == "3"
|
|
|
|
def test_type_literal(self):
|
|
"""EventTrigger type should always be 'event'."""
|
|
trigger = EventTrigger(event_type="custom")
|
|
assert trigger.type == "event"
|
|
|
|
|
|
class TestScheduleTrigger:
|
|
"""Tests for ScheduleTrigger model."""
|
|
|
|
def test_with_cron(self):
|
|
"""ScheduleTrigger should accept cron expression."""
|
|
trigger = ScheduleTrigger(cron="0 9 * * *")
|
|
assert trigger.type == "schedule"
|
|
assert trigger.cron == "0 9 * * *"
|
|
assert trigger.interval_seconds is None
|
|
|
|
def test_with_interval_seconds(self):
|
|
"""ScheduleTrigger should accept interval_seconds."""
|
|
trigger = ScheduleTrigger(interval_seconds=60)
|
|
assert trigger.interval_seconds == 60
|
|
assert trigger.cron is None
|
|
|
|
def test_accepts_schedule_alias(self):
|
|
"""ScheduleTrigger should accept legacy schedule alias for cron."""
|
|
trigger = ScheduleTrigger(schedule="0 */5 * * * *")
|
|
assert trigger.cron == "0 */5 * * * *"
|
|
assert trigger.schedule == "0 */5 * * * *"
|
|
|
|
def test_requires_exactly_one_strategy(self):
|
|
"""ScheduleTrigger must have exactly one of cron or interval_seconds."""
|
|
# Neither provided should raise
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ScheduleTrigger()
|
|
assert "必须且只能有一个非 null" in str(exc_info.value)
|
|
|
|
# Both provided should raise
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ScheduleTrigger(cron="* * * * *", interval_seconds=10)
|
|
assert "必须且只能有一个非 null" in str(exc_info.value)
|
|
|
|
def test_valid_cron_expressions(self):
|
|
"""ScheduleTrigger should accept various cron expressions."""
|
|
trigger1 = ScheduleTrigger(cron="*/5 * * * *")
|
|
assert trigger1.cron == "*/5 * * * *"
|
|
|
|
trigger2 = ScheduleTrigger(cron="0 0 1 1 *")
|
|
assert trigger2.cron == "0 0 1 1 *"
|
|
|
|
def test_valid_intervals(self):
|
|
"""ScheduleTrigger should accept various intervals."""
|
|
trigger1 = ScheduleTrigger(interval_seconds=30)
|
|
assert trigger1.interval_seconds == 30
|
|
|
|
trigger2 = ScheduleTrigger(interval_seconds=3600)
|
|
assert trigger2.interval_seconds == 3600
|
|
|
|
|
|
class TestHandlerDescriptor:
|
|
"""Tests for HandlerDescriptor model."""
|
|
|
|
def test_required_id_and_trigger(self):
|
|
"""HandlerDescriptor requires id and trigger."""
|
|
trigger = CommandTrigger(command="hello")
|
|
handler = HandlerDescriptor(id="test.handler", trigger=trigger)
|
|
assert handler.id == "test.handler"
|
|
assert handler.trigger == trigger
|
|
assert handler.priority == 0
|
|
assert handler.permissions.require_admin is False
|
|
|
|
def test_with_priority_and_permissions(self):
|
|
"""HandlerDescriptor should accept priority and permissions."""
|
|
trigger = CommandTrigger(command="admin")
|
|
perms = Permissions(require_admin=True, level=5)
|
|
handler = HandlerDescriptor(
|
|
id="admin.handler",
|
|
trigger=trigger,
|
|
priority=10,
|
|
permissions=perms,
|
|
)
|
|
assert handler.priority == 10
|
|
assert handler.permissions.require_admin is True
|
|
assert handler.permissions.level == 5
|
|
|
|
def test_with_event_trigger(self):
|
|
"""HandlerDescriptor should work with EventTrigger."""
|
|
trigger = EventTrigger(event_type="message")
|
|
handler = HandlerDescriptor(id="event.handler", trigger=trigger)
|
|
assert handler.trigger.type == "event"
|
|
assert handler.trigger.event_type == "message"
|
|
|
|
def test_with_schedule_trigger(self):
|
|
"""HandlerDescriptor should work with ScheduleTrigger."""
|
|
trigger = ScheduleTrigger(cron="0 9 * * *")
|
|
handler = HandlerDescriptor(id="scheduled.handler", trigger=trigger)
|
|
assert handler.trigger.type == "schedule"
|
|
assert handler.trigger.cron == "0 9 * * *"
|
|
|
|
def test_model_dump(self):
|
|
"""HandlerDescriptor should serialize correctly."""
|
|
trigger = CommandTrigger(command="test", aliases=["t"])
|
|
perms = Permissions(require_admin=True, level=5)
|
|
handler = HandlerDescriptor(
|
|
id="test.handler",
|
|
trigger=trigger,
|
|
priority=10,
|
|
permissions=perms,
|
|
)
|
|
data = handler.model_dump()
|
|
assert data["id"] == "test.handler"
|
|
assert data["priority"] == 10
|
|
assert data["trigger"]["type"] == "command"
|
|
assert data["trigger"]["command"] == "test"
|
|
assert data["permissions"]["require_admin"] is True
|
|
|
|
def test_extra_fields_forbidden(self):
|
|
"""HandlerDescriptor should forbid extra fields."""
|
|
trigger = CommandTrigger(command="test")
|
|
with pytest.raises(ValidationError):
|
|
HandlerDescriptor(id="test", trigger=trigger, extra="field")
|
|
|
|
def test_defaults_kind_and_contract_from_trigger(self):
|
|
"""HandlerDescriptor should infer contract defaults from trigger shape."""
|
|
message_handler = HandlerDescriptor(
|
|
id="msg.handler",
|
|
trigger=CommandTrigger(command="hello"),
|
|
)
|
|
schedule_handler = HandlerDescriptor(
|
|
id="sched.handler",
|
|
trigger=ScheduleTrigger(interval_seconds=30),
|
|
)
|
|
|
|
assert message_handler.kind == "handler"
|
|
assert message_handler.contract == "message_event"
|
|
assert schedule_handler.contract == "schedule"
|
|
|
|
|
|
class TestSessionRef:
|
|
"""Tests for SessionRef model."""
|
|
|
|
def test_accepts_legacy_session_alias(self):
|
|
"""SessionRef should accept legacy session field while normalizing storage."""
|
|
ref = SessionRef.model_validate({"session": "session-1", "platform": "qq"})
|
|
|
|
assert ref.conversation_id == "session-1"
|
|
assert ref.session == "session-1"
|
|
assert ref.platform == "qq"
|
|
|
|
def test_schema_is_exported_for_platform_targets(self):
|
|
"""SessionRef schema should remain a shared protocol constant."""
|
|
assert SESSION_REF_SCHEMA["required"] == ["conversation_id"]
|
|
|
|
|
|
class TestCapabilityDescriptor:
|
|
"""Tests for CapabilityDescriptor model."""
|
|
|
|
def test_required_name_and_description(self):
|
|
"""CapabilityDescriptor requires name and description."""
|
|
cap = CapabilityDescriptor(name="custom.chat", description="Chat with LLM")
|
|
assert cap.name == "custom.chat"
|
|
assert cap.description == "Chat with LLM"
|
|
assert cap.input_schema is None
|
|
assert cap.output_schema is None
|
|
assert cap.supports_stream is False
|
|
assert cap.cancelable is False
|
|
|
|
def test_builtin_capability_requires_schemas(self):
|
|
"""Built-in capabilities should enforce schema governance."""
|
|
with pytest.raises(ValidationError, match="必须同时提供"):
|
|
CapabilityDescriptor(name="llm.chat", description="missing schemas")
|
|
|
|
def test_builtin_capability_requires_registry_schema_match(self):
|
|
"""Built-in capabilities should reject schemas that drift from protocol constants."""
|
|
with pytest.raises(ValidationError, match="协议注册表保持一致"):
|
|
CapabilityDescriptor(
|
|
name="llm.chat",
|
|
description="wrong schema",
|
|
input_schema={"type": "object", "properties": {}, "required": []},
|
|
output_schema=BUILTIN_CAPABILITY_SCHEMAS["llm.chat"]["output"],
|
|
)
|
|
|
|
def test_builtin_capability_schema_registry_contains_required_entries(self):
|
|
"""Built-in schema registry should cover documented core capabilities."""
|
|
assert "llm.chat" in BUILTIN_CAPABILITY_SCHEMAS
|
|
assert "db.list" in BUILTIN_CAPABILITY_SCHEMAS
|
|
assert LLM_CHAT_INPUT_SCHEMA["required"] == ["prompt"]
|
|
assert (
|
|
DB_LIST_INPUT_SCHEMA["properties"]["prefix"]["anyOf"][1]["type"] == "null"
|
|
)
|
|
|
|
def test_reserved_capability_prefixes_are_protocol_constants(self):
|
|
"""Reserved namespace prefixes should live in protocol descriptors."""
|
|
assert RESERVED_CAPABILITY_PREFIXES == ("handler.", "system.", "internal.")
|
|
|
|
def test_with_schemas(self):
|
|
"""CapabilityDescriptor should accept input/output schemas."""
|
|
cap = CapabilityDescriptor(
|
|
name="db.query",
|
|
description="Query database",
|
|
input_schema={"type": "object", "properties": {"sql": {"type": "string"}}},
|
|
output_schema={"type": "array"},
|
|
)
|
|
assert cap.input_schema["type"] == "object"
|
|
assert cap.output_schema["type"] == "array"
|
|
|
|
def test_with_stream_and_cancelable(self):
|
|
"""CapabilityDescriptor should accept stream and cancelable flags."""
|
|
cap = CapabilityDescriptor(
|
|
name="llm.stream",
|
|
description="Stream chat",
|
|
supports_stream=True,
|
|
cancelable=True,
|
|
)
|
|
assert cap.supports_stream is True
|
|
assert cap.cancelable is True
|
|
|
|
def test_model_dump(self):
|
|
"""CapabilityDescriptor should serialize correctly."""
|
|
cap = CapabilityDescriptor(
|
|
name="test.cap",
|
|
description="Test capability",
|
|
supports_stream=True,
|
|
)
|
|
data = cap.model_dump()
|
|
assert data["name"] == "test.cap"
|
|
assert data["description"] == "Test capability"
|
|
assert data["supports_stream"] is True
|
|
|
|
def test_extra_fields_forbidden(self):
|
|
"""CapabilityDescriptor should forbid extra fields."""
|
|
with pytest.raises(ValidationError):
|
|
CapabilityDescriptor(
|
|
name="test",
|
|
description="Test",
|
|
extra="field",
|
|
)
|
|
|
|
|
|
class TestTriggerDiscriminator:
|
|
"""Tests for Trigger type discriminator."""
|
|
|
|
def test_command_trigger_discrimination(self):
|
|
"""CommandTrigger should be correctly discriminated."""
|
|
handler = HandlerDescriptor(
|
|
id="cmd.handler",
|
|
trigger={"type": "command", "command": "test"},
|
|
)
|
|
assert isinstance(handler.trigger, CommandTrigger)
|
|
assert handler.trigger.command == "test"
|
|
|
|
def test_message_trigger_discrimination(self):
|
|
"""MessageTrigger should be correctly discriminated."""
|
|
handler = HandlerDescriptor(
|
|
id="msg.handler",
|
|
trigger={"type": "message", "keywords": ["hello"]},
|
|
)
|
|
assert isinstance(handler.trigger, MessageTrigger)
|
|
assert handler.trigger.keywords == ["hello"]
|
|
|
|
def test_event_trigger_discrimination(self):
|
|
"""EventTrigger should be correctly discriminated."""
|
|
handler = HandlerDescriptor(
|
|
id="evt.handler",
|
|
trigger={"type": "event", "event_type": "message"},
|
|
)
|
|
assert isinstance(handler.trigger, EventTrigger)
|
|
assert handler.trigger.event_type == "message"
|
|
|
|
def test_schedule_trigger_discrimination(self):
|
|
"""ScheduleTrigger should be correctly discriminated."""
|
|
handler = HandlerDescriptor(
|
|
id="sched.handler",
|
|
trigger={"type": "schedule", "cron": "0 9 * * *"},
|
|
)
|
|
assert isinstance(handler.trigger, ScheduleTrigger)
|
|
assert handler.trigger.cron == "0 9 * * *"
|