Files
quercus/tests/core/test_schemas.py
T
Mike-Solar e0db5ab2cf P1: host-agnostic quercus-core with agent loop, providers, and tool schemas
- Core types: Rational time, EntityId, ActionBatch, PngBytes (no Pillow dep)
- Config: ~/.quercus/config.toml with 0600 perms, env var override for CI
- LLMProvider: Claude (anthropic SDK) and OpenAI-compatible endpoint
  (covers custom gateways and llama.cpp server mode); graceful degradation
  without API keys
- Tool schemas: 26 curated tools with JSON Schema and read_only flags
- AgentLoop: sync dialogue loop with confirmation gate (read-only batches
  bypass, rejected mutations are reported back to the LLM), PNG frame
  feedback for multimodal providers
- Session log: JSONL recording with frame sha256 hashes, replayable
- HostAdapter ABC (signatures only, aligned with OPP/1 method families)
- Mock LLM provider (scripted/replay/record) for network-free CI

Tests: 69 passed via uv run pytest (no network access)
2026-08-25 02:06:10 +08:00

113 lines
3.2 KiB
Python

"""工具 schema 注册表:合法性、read_only 标注、有理秒参数形状。"""
import json
import pytest
from quercus_core.providers.base import ToolSchema
from quercus_core.tools.schemas import (
RATIONAL_SCHEMA,
TIME_RANGE_SCHEMA,
TOOL_LIST,
TOOLS,
get_tool,
tool_read_only,
)
# 期望的 read_only 标注(独立于代码本身,防止误改)。
EXPECTED_READ_ONLY = {
"open_project": False,
"save_project": False,
"get_project_overview": True,
"probe_media": True,
"import_footage": False,
"list_footage": True,
"add_track": False,
"place_clip": False,
"split_clip": False,
"trim_clip": False,
"move_clip": False,
"ripple_delete": False,
"add_transition": False,
"add_marker": False,
"add_effect": False,
"set_param": False,
"set_keyframe": False,
"list_effects": True,
"get_frame": True,
"scan_timeline": True,
"get_audio_levels": True,
"play": True,
"pause": True,
"seek": True,
"export_render": False,
"undo_last_action": False,
}
def test_tool_count():
assert len(TOOLS) == 26
assert len(TOOL_LIST) == 26
assert len({t.name for t in TOOL_LIST}) == 26
def test_schema_shape():
for name, tool in TOOLS.items():
assert isinstance(tool, ToolSchema)
assert tool.name == name
assert isinstance(tool.description, str) and tool.description
p = tool.parameters
assert p["type"] == "object"
assert isinstance(p["properties"], dict)
required = p.get("required", [])
assert isinstance(required, list)
for req in required:
assert req in p["properties"], f"{name}: required {req} 不在 properties"
json.dumps(p) # 可 JSON 序列化 => 合法 JSON Schema 对象
def test_read_only_flags():
assert set(TOOLS) == set(EXPECTED_READ_ONLY)
for name, expected in EXPECTED_READ_ONLY.items():
assert TOOLS[name].read_only is expected, f"{name} read_only 应为 {expected}"
assert tool_read_only(name) is expected, f"{name} tool_read_only 不一致"
def test_unknown_tool_treated_as_mutation():
assert tool_read_only("no_such_tool") is False
@pytest.mark.parametrize(
"tool_name,param_name",
[
("place_clip", "time"),
("split_clip", "time"),
("trim_clip", "in_point"),
("move_clip", "time"),
("add_marker", "time"),
("set_keyframe", "time"),
("get_frame", "time"),
("seek", "time"),
],
)
def test_rational_params_are_num_den(tool_name, param_name):
props = TOOLS[tool_name].parameters["properties"]
assert props[param_name] == RATIONAL_SCHEMA, f"{tool_name}.{param_name}"
def test_time_range_param_shape():
props = TOOLS["scan_timeline"].parameters["properties"]
assert props["range"] == TIME_RANGE_SCHEMA
assert props["count"]["maximum"] == 64
def test_get_frame_max_size():
props = TOOLS["get_frame"].parameters["properties"]
max_size = props["max_size"]
assert max_size["type"] == "object"
assert max_size["required"] == ["width", "height"]
def test_get_tool_lookup():
assert get_tool("play") is TOOLS["play"]
assert get_tool("nope") is None