- 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)
113 lines
3.2 KiB
Python
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
|