Files
turnstone/tests/test_tools_schema.py
T
Patrick Buckley dc464ac313 feat: Agent Skills standard compliance + frontend spec fields
Brings skills implementation into full compliance with agentskills.io:

Parser:
- Read `allowed-tools` (hyphenated, standard) only; stored as
  `allowed_tools` internally — no underscore fallback
- Reject consecutive hyphens in skill names
- Extract author/version from standard `metadata:` map with top-level
  fallback; null-safe (no "None" string for bare YAML keys)
- Truncate description at 1024 chars, compatibility at 500 chars (spec
  caps) with log warnings
- Lenient parsing mode (lenient=True) for cross-client import: sanitizes
  names, returns None on skip, malformed-YAML colon-value retry
- Type overloads: strict mode returns ParsedSkill, lenient returns
  ParsedSkill | None

Session:
- `<available-skills>` XML catalog in system messages for
  activation="search" skills (disabled ones filtered out, capped at 30)

Tool rename:
- `load_skill` tool → `skill` (JSON, session preparers/executors,
  approval labels, tests, docs)

Storage (migration 023):
- Add `license` and `compatibility` columns to prompt_templates
- skill_license / compatibility params on create_prompt_template across
  protocol, SQLite, PostgreSQL backends
- Add to SKILL_MUTABLE for update_prompt_template

API + server:
- SkillInfo, CreateSkillRequest, UpdateSkillRequest: license +
  compatibility fields
- Create/update/install endpoints extract and persist both fields
- Install endpoint maps parsed.license + parsed.compatibility from
  imported SKILL.md (previously discarded)
- _skill_to_response() includes both fields

Admin UI:
- Create + edit modals: version, license, compatibility fields
- Readonly (imported) skills: "edit" → "view" button, modal title
  "View Skill", all fields disabled, Save hidden, Cancel → "Close",
  collapsibles auto-expand, focus on Close button
- :disabled CSS for dark-theme modal inputs (bg-highlight, cursor
  not-allowed, dimmed text)
- Fix addEventListener stacking on auto-approve checkboxes → .onchange

SDK: license + compatibility on SkillInfo, CreateSkillRequest,
UpdateSkillRequest TypeScript interfaces

Docs: governance.md, judge.md, tools.md, README, diagram updated
2026-03-17 16:09:06 -07:00

130 lines
4.3 KiB
Python

"""Tests for turnstone.core.tools — JSON auto-loading and schema validation."""
from turnstone.core.tools import (
_META,
AGENT_AUTO_TOOLS,
AGENT_TOOLS,
PRIMARY_KEY_MAP,
TASK_AGENT_TOOLS,
TASK_AUTO_TOOLS,
TOOLS,
)
class TestToolsSchema:
def test_all_tools_have_function_type(self):
for tool in TOOLS:
assert tool["type"] == "function", f"Tool missing type='function': {tool}"
def test_all_tools_have_name(self):
for tool in TOOLS:
assert "name" in tool["function"], f"Tool missing name: {tool}"
assert isinstance(tool["function"]["name"], str)
def test_all_tools_have_description(self):
for tool in TOOLS:
assert "description" in tool["function"], f"Tool missing description: {tool}"
assert len(tool["function"]["description"]) > 0
def test_all_tools_have_parameters(self):
for tool in TOOLS:
params = tool["function"]["parameters"]
assert params["type"] == "object"
assert "properties" in params
def test_required_fields_exist_in_properties(self):
for tool in TOOLS:
func = tool["function"]
params = func["parameters"]
required = params.get("required", [])
properties = params["properties"]
for field in required:
assert field in properties, (
f"Tool '{func['name']}': required field '{field}' not in properties"
)
def test_tool_names_unique(self):
names = [t["function"]["name"] for t in TOOLS]
assert len(names) == len(set(names)), f"Duplicate tool names: {names}"
def test_agent_tools_subset(self):
tool_names = {t["function"]["name"] for t in TOOLS}
agent_names = {t["function"]["name"] for t in AGENT_TOOLS}
assert agent_names.issubset(tool_names), (
f"AGENT_TOOLS has names not in TOOLS: {agent_names - tool_names}"
)
def test_task_agent_tools_subset(self):
tool_names = {t["function"]["name"] for t in TOOLS}
task_names = {t["function"]["name"] for t in TASK_AGENT_TOOLS}
assert task_names.issubset(tool_names), (
f"TASK_AGENT_TOOLS has names not in TOOLS: {task_names - tool_names}"
)
def test_agent_tools_not_empty(self):
assert len(AGENT_TOOLS) > 0
def test_task_agent_tools_not_empty(self):
assert len(TASK_AGENT_TOOLS) > 0
class TestToolsMetadata:
"""Validate the metadata extracted from JSON files."""
def test_tool_count(self):
assert len(TOOLS) == 18
def test_agent_tools_count(self):
assert len(AGENT_TOOLS) == 9
def test_task_agent_tools_count(self):
assert len(TASK_AGENT_TOOLS) == 12
def test_auto_approve_sets_match(self):
expected = {
"read_file",
"search",
"math",
"man",
"web_fetch",
"web_search",
"notify",
}
assert expected == AGENT_AUTO_TOOLS
assert expected == TASK_AUTO_TOOLS
def test_primary_key_map(self):
expected = {
"bash": "command",
"math": "code",
"read_file": "path",
"search": "query",
"write_file": "content",
"edit_file": "old_string",
"man": "page",
"web_fetch": "url",
"web_search": "query",
"task": "prompt",
"create_plan": "goal",
"memory": "name",
"recall": "query",
"notify": "message",
"watch": "command",
"read_resource": "uri",
"use_prompt": "name",
"skill": "name",
}
assert expected == PRIMARY_KEY_MAP
def test_no_metadata_in_function_dicts(self):
"""Ensure turnstone metadata keys are stripped from the OpenAI schema."""
meta_keys = {"agent", "task_agent", "auto_approve", "primary_key"}
for tool in TOOLS:
func = tool["function"]
leaked = meta_keys & set(func)
assert not leaked, f"Tool '{func['name']}' leaks metadata into function dict: {leaked}"
def test_meta_has_all_tools(self):
tool_names = {t["function"]["name"] for t in TOOLS}
assert set(_META.keys()) == tool_names