mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
016ce473f4
Code-quality pass over the PR's own changes: - The bare<->qualified email aliasing rule lived inline in the generic dispatcher (_execute_tool_block_impl). It is policy knowledge, so it moves next to BUILTIN_EMAIL_TOOLS as email_tool_policy_names(); the dispatcher just consumes it, and the rule gets its own unit test (including the mcp__email__<not-a-tool> and mcp__other__ non-alias cases). - The default Assistant's enabled_tools list was an inline literal inside the CrewMember seed, and its registry-sync test asserted a source-code substring. Extracted to DEFAULT_ASSISTANT_ENABLED_TOOLS so the test imports and checks the actual value. - _fenced_tool_call return type tightened to Optional[Tuple[str, str]]. No behavior change; suite green (3295 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
139 lines
5.5 KiB
Python
139 lines
5.5 KiB
Python
"""PR #3681 — every email-tool registry derives from / covers BUILTIN_EMAIL_TOOLS.
|
|
|
|
Three review rounds on #3681 each found another hand-maintained copy of the
|
|
email tool list that had drifted (NON_ADMIN_BLOCKED_TOOLS, the `disable_tool
|
|
email` alias, tool_schemas' private copy). This test pins ALL of them to the
|
|
single source of truth, including the email MCP server itself, so adding or
|
|
removing an email tool fails loudly everywhere it matters instead of silently
|
|
shrinking one surface.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import src.agent_tools # noqa: F401 — resolve the circular-import cluster first
|
|
from src.tool_security import (
|
|
BUILTIN_EMAIL_TOOLS,
|
|
NON_ADMIN_BLOCKED_TOOLS,
|
|
PLAN_MODE_READONLY_TOOLS,
|
|
_PLAN_MODE_KNOWN_MUTATORS,
|
|
)
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
# Read-only email tools — the only ones plan mode may execute.
|
|
_READONLY_EMAIL = {"list_email_accounts", "list_emails", "read_email", "search_emails"}
|
|
|
|
|
|
def test_email_server_tools_match_builtin_set():
|
|
"""BUILTIN_EMAIL_TOOLS must equal exactly what the email server exposes."""
|
|
source = (_REPO_ROOT / "mcp_servers" / "email_server.py").read_text()
|
|
served = set(re.findall(r'Tool\(\s*name="(\w+)"', source))
|
|
assert served == set(BUILTIN_EMAIL_TOOLS), (
|
|
f"email_server tools != BUILTIN_EMAIL_TOOLS; "
|
|
f"server-only: {sorted(served - BUILTIN_EMAIL_TOOLS)}, "
|
|
f"set-only: {sorted(BUILTIN_EMAIL_TOOLS - served)}"
|
|
)
|
|
|
|
|
|
def test_fence_tags_cover_email_tools():
|
|
from src.agent_tools import TOOL_TAGS
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= set(TOOL_TAGS)
|
|
|
|
|
|
def test_function_schemas_cover_email_tools():
|
|
"""Every email tool must be advertised to native function-calling models."""
|
|
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
|
|
|
|
names = {(t.get("function") or {}).get("name") for t in FUNCTION_TOOL_SCHEMAS}
|
|
missing = set(BUILTIN_EMAIL_TOOLS) - names
|
|
assert not missing, f"email tools without native schemas: {sorted(missing)}"
|
|
|
|
|
|
def test_prompt_sections_cover_email_tools():
|
|
"""Every email tool must be describable to fenced-block models."""
|
|
from src.agent_loop import TOOL_SECTIONS
|
|
|
|
missing = set(BUILTIN_EMAIL_TOOLS) - set(TOOL_SECTIONS)
|
|
assert not missing, f"email tools without prompt sections: {sorted(missing)}"
|
|
|
|
|
|
def test_domain_rules_map_covers_email_tools():
|
|
from src.agent_loop import _DOMAIN_TOOL_MAP
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= _DOMAIN_TOOL_MAP["email"]
|
|
|
|
|
|
def test_rag_descriptions_and_keyword_hints_cover_email_tools():
|
|
"""Every email tool must be retrievable (embedding index + keyword hints)."""
|
|
from src.tool_index import (
|
|
ASSISTANT_ALWAYS_AVAILABLE,
|
|
BUILTIN_TOOL_DESCRIPTIONS,
|
|
)
|
|
|
|
missing = set(BUILTIN_EMAIL_TOOLS) - set(BUILTIN_TOOL_DESCRIPTIONS)
|
|
assert not missing, f"email tools not in the RAG description index: {sorted(missing)}"
|
|
assert BUILTIN_EMAIL_TOOLS <= ASSISTANT_ALWAYS_AVAILABLE
|
|
|
|
|
|
def test_known_tool_names_cover_email_tools():
|
|
from src.tool_policy import _COMMON_TOOL_NAMES
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= _COMMON_TOOL_NAMES
|
|
|
|
|
|
def test_non_admin_blocklist_covers_email_tools():
|
|
assert BUILTIN_EMAIL_TOOLS <= NON_ADMIN_BLOCKED_TOOLS
|
|
|
|
|
|
def test_plan_mode_partitions_email_tools():
|
|
"""Plan mode must classify every email tool: the read-only four are
|
|
allowed (both spellings, given the bare/qualified alias gate), everything
|
|
else is in the fail-closed mutator backstop. An unclassified tool would be
|
|
allowed bare and blocked qualified — the round-2 aliasing bug again."""
|
|
readonly = set(BUILTIN_EMAIL_TOOLS) & PLAN_MODE_READONLY_TOOLS
|
|
assert readonly == _READONLY_EMAIL, (
|
|
f"plan-mode read-only email subset drifted: {sorted(readonly)}"
|
|
)
|
|
mutators = set(BUILTIN_EMAIL_TOOLS) - _READONLY_EMAIL
|
|
missing = mutators - _PLAN_MODE_KNOWN_MUTATORS
|
|
assert not missing, f"mutating email tools not in plan-mode backstop: {sorted(missing)}"
|
|
|
|
|
|
def test_frontend_tool_selector_covers_email_tools():
|
|
"""The assistant tool-selector UI must be able to grant/revoke every email
|
|
tool — a name missing here can never be toggled from the UI."""
|
|
source = (_REPO_ROOT / "static" / "js" / "assistant.js").read_text()
|
|
m = re.search(r"'Email':\s*\[([^\]]*)\]", source)
|
|
assert m, "Email group not found in assistant.js TOOL_GROUPS"
|
|
listed = set(re.findall(r"'(\w+)'", m.group(1)))
|
|
assert listed == set(BUILTIN_EMAIL_TOOLS), (
|
|
f"assistant.js Email group != BUILTIN_EMAIL_TOOLS; "
|
|
f"js-only: {sorted(listed - BUILTIN_EMAIL_TOOLS)}, "
|
|
f"missing: {sorted(BUILTIN_EMAIL_TOOLS - listed)}"
|
|
)
|
|
|
|
|
|
def test_default_assistant_seed_covers_email_tools():
|
|
"""The default Assistant crew seed grants the full email set."""
|
|
from src.task_scheduler import DEFAULT_ASSISTANT_ENABLED_TOOLS
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= set(DEFAULT_ASSISTANT_ENABLED_TOOLS)
|
|
|
|
|
|
def test_email_policy_name_aliases():
|
|
"""The alias rule every execution gate relies on."""
|
|
from src.tool_security import email_tool_policy_names
|
|
|
|
assert email_tool_policy_names("list_emails") == {
|
|
"list_emails", "mcp__email__list_emails",
|
|
}
|
|
assert email_tool_policy_names("mcp__email__delete_email") == {
|
|
"delete_email", "mcp__email__delete_email",
|
|
}
|
|
# Non-email names alias only to themselves — including mcp__email__
|
|
# spellings of tools the email server doesn't expose.
|
|
assert email_tool_policy_names("bash") == {"bash"}
|
|
assert email_tool_policy_names("mcp__email__not_a_tool") == {"mcp__email__not_a_tool"}
|
|
assert email_tool_policy_names("mcp__other__list_emails") == {"mcp__other__list_emails"}
|