Simplify Athena runtime and document current architecture

This commit is contained in:
Mikei386
2026-08-30 08:45:52 +02:00
parent c721db47d0
commit c6517ee137
56 changed files with 1275 additions and 4713 deletions
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env python3
"""Dependency-free safety checks for the hass_mcp YAML overlay."""
from __future__ import annotations
import importlib.util
import pathlib
import sys
import types
def module(name: str, **attributes):
value = types.ModuleType(name)
for key, item in attributes.items():
setattr(value, key, item)
sys.modules[name] = value
return value
class ToolError(Exception):
pass
def decorator(**_kwargs):
return lambda function: function
module("homeassistant")
module("homeassistant.core", HomeAssistant=object)
module("homeassistant.util", slugify=lambda value: str(value).lower().replace(" ", "_"))
module("guarded")
module("guarded.tools")
module("guarded.identity", user_context=lambda: None)
module("guarded.protocol", ToolError=ToolError, internal_error=lambda message, error: RuntimeError(f"{message}: {error}"))
module(
"guarded.registry",
LIMIT_FIELD={"type": "integer"},
OFFSET_FIELD={"type": "integer"},
paginate=lambda items, limit, offset: {"items": items[offset : offset + limit]},
schema=lambda **kwargs: kwargs,
tool=decorator,
)
source = pathlib.Path(__file__).parents[1] / "platform/mcp/patches/hass_mcp/yaml_config.py"
spec = importlib.util.spec_from_file_location("guarded.tools.yaml_config", source)
assert spec and spec.loader
guard = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = guard
spec.loader.exec_module(guard)
assert set(guard._KINDS) == {"automation", "script", "scene", "configuration"}
assert all("secret" not in filename for filename, _, _ in guard._KINDS.values())
assert guard._redact_line("api_key: abc") == "api_key: <redacted>"
assert guard._redact_line("token: abc") == "token: <redacted>"
assert guard._redact_line("value: !secret private_name") == "value: !secret <redacted-reference>"
assert guard._redact_line("alias: Safe automation") == "alias: Safe automation"
for sensitive in ("token: abc", "password: abc", "value: !secret private_name"):
try:
guard._reject_sensitive_replacement(sensitive)
except ToolError:
pass
else:
raise AssertionError(f"sensitive replacement was accepted: {sensitive}")
change = guard._change_record("automation", "update", "before", {"id": "demo"})
preview = guard._preview(change)
assert preview["changed"] is False
assert preview["confirmation_required"] is True
guard._consume_ticket(change, preview["approval_ticket"])
try:
guard._consume_ticket(change, preview["approval_ticket"])
except ToolError:
pass
else:
raise AssertionError("one-time approval ticket was reusable")
other = guard._change_record("automation", "update", "different", {"id": "demo"})
ticket = guard._preview(change)["approval_ticket"]
try:
guard._consume_ticket(other, ticket)
except ToolError:
pass
else:
raise AssertionError("ticket accepted a different current file fingerprint")
diff = guard._source_diff("automations.yaml", "a\nb\n", "a\nc\n")
assert any("-b" in line for line in diff)
assert any("+c" in line for line in diff)
print("hass_mcp_yaml_guard_tests=ok")
-106
View File
@@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""Tests for the single declarative MCP client registry."""
from __future__ import annotations
import importlib.util
import json
import sqlite3
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).parents[1]
SOURCE = ROOT / "platform/mcp/sync-clients.py"
def load_module():
spec = importlib.util.spec_from_file_location("sync_clients", SOURCE)
module = importlib.util.module_from_spec(spec)
assert spec.loader
spec.loader.exec_module(module)
return module
class RegistryTests(unittest.TestCase):
def setUp(self):
self.module = load_module()
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.registry = self.root / "registry.json"
self.registry.write_text(json.dumps({"version": 1, "servers": [{
"id": "one", "name": "One", "description": "Test", "url": "http://one/mcp",
"clients": ["hermes", "openwebui"], "timeout": 123,
}]}))
def tearDown(self):
self.temp.cleanup()
def test_same_registry_generates_both_clients(self):
items = self.module.active(self.registry, "hermes")
block = self.module.hermes_block(items)
self.assertIn("one:", block)
db = self.root / "webui.db"
con = sqlite3.connect(db)
con.execute("create table config (key text primary key, value text, updated_at integer)")
con.commit(); con.close()
self.module.update_openwebui(db, self.module.active(self.registry, "openwebui"))
con = sqlite3.connect(db)
value = json.loads(con.execute("select value from config where key='tool_server.connections'").fetchone()[0])
con.close()
self.assertEqual(value[0]["info"]["id"], "one")
def test_old_platform_context_registration_is_removed(self):
db = self.root / "webui.db"
con = sqlite3.connect(db)
con.execute("create table config (key text primary key, value text, updated_at integer)")
con.execute("insert into config values (?,?,?)", ("tool_server.connections", json.dumps([
{"info": {"id": "athena-platform"}, "url": "http://old/mcp"},
{"info": {"id": "unmanaged"}, "url": "http://keep/mcp"},
]), 0))
con.commit(); con.close()
self.module.update_openwebui(db, self.module.active(self.registry, "openwebui"))
con = sqlite3.connect(db)
ids = [item["info"]["id"] for item in json.loads(con.execute("select value from config where key='tool_server.connections'").fetchone()[0])]
con.close()
self.assertEqual(ids, ["unmanaged", "one"])
def test_production_registry_has_unique_ids_and_fritzbox(self):
document = json.loads((ROOT / "config/mcp-registry.json").read_text())
ids = [item["id"] for item in document["servers"]]
hermes_ids = [
item.get("hermes_id", item["id"])
for item in document["servers"] if "hermes" in item.get("clients", [])
]
self.assertEqual(len(ids), len(set(ids)))
self.assertEqual(len(hermes_ids), len(set(hermes_ids)))
fritz = next(item for item in document["servers"] if item["id"] == "fritzbox-local")
self.assertEqual(fritz["hub"]["type"], "stdio")
self.assertIn("fritz-mcp", fritz["hub"]["args"][-1])
self.assertEqual(len(fritz["tool_include"]), 4)
def test_hermes_tool_filter_is_generated(self):
item = {
"id": "wide", "name": "Wide", "description": "Test",
"url": "http://wide/mcp", "clients": ["hermes"],
"tool_include": ["list", "describe", "call"],
}
block = self.module.hermes_block([item])
self.assertIn(" tools:\n include:", block)
self.assertIn(' - "describe"', block)
def test_raw_mcphub_token_can_replace_host_specific_env_file(self):
item = {
"id": "hub", "name": "Hub", "description": "Test",
"url": "http://hub/mcp/test", "clients": ["hermes"],
"env_file": "/missing/client.env",
"key_env": "MCPHUB_BEARER_TOKEN",
}
self.module.CLIENT_TOKEN = "local-token"
self.assertTrue(self.module.enabled(item))
self.assertEqual(self.module.resolved(item), ("http://hub/mcp/test", "local-token"))
if __name__ == "__main__":
unittest.main()
-91
View File
@@ -1,91 +0,0 @@
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import pathlib
import tempfile
import unittest
SOURCE = pathlib.Path(__file__).parents[1] / "platform/mcphub/deploy-extension.py"
SPEC = importlib.util.spec_from_file_location("deploy_extension", SOURCE)
assert SPEC and SPEC.loader
deploy = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(deploy)
class DeployExtensionTest(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.root = pathlib.Path(self.temp.name)
self.appdata = self.root / "appdata"
self.work = self.appdata / "work/example"
self.secrets = self.root / "secrets"
self.registry = self.appdata / "config/mcp-registry.json"
self.work.mkdir(parents=True)
self.secrets.mkdir()
self.registry.parent.mkdir(parents=True)
self.registry.write_text('{"version":1,"servers":[]}\n')
artifact = self.work / "index.js"
artifact.write_text("console.log('ok')\n")
digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
self.manifest = self.work / "manifest.json"
self.manifest.write_text(json.dumps({
"server": {
"id": "example", "hermes_id": "example", "name": "Example",
"description": "Example MCP", "url": "http://host/mcp/example",
"clients": ["hermes"],
"deployment": {
"required_env": ["EXAMPLE_TOKEN"],
"required_files": ["example-config.toml"],
},
"hub": {
"type": "stdio", "secret_file": "example.env",
"command": "node", "args": ["/app/data/extensions/example/index.js"],
"enabled": True,
},
},
"artifacts": [{
"source": str(artifact), "path": "index.js",
"sha256": digest, "mode": "0644",
}],
}))
def tearDown(self) -> None:
self.temp.cleanup()
def args(self, **extra: object) -> argparse.Namespace:
values = {
"appdata": self.appdata, "registry": self.registry,
"secrets": self.secrets, "manifest": self.manifest, "id": "example",
"skip_api": True,
}
values.update(extra)
return argparse.Namespace(**values)
def registered(self) -> dict:
return json.loads(self.registry.read_text())["servers"][0]
def test_missing_secret_forces_disabled_and_unpublished(self) -> None:
deploy.stage(self.args())
server = self.registered()
self.assertFalse(server["hub"]["enabled"])
self.assertEqual(server["clients"], [])
self.assertTrue((self.appdata / "extensions/example/index.js").is_file())
with self.assertRaises(SystemExit):
deploy.set_enabled(self.args(), True)
def test_complete_secret_allows_activation(self) -> None:
(self.secrets / "example.env").write_text("EXAMPLE_TOKEN=value\n")
(self.secrets / "example-config.toml").write_text("account = 'example'\n")
deploy.stage(self.args())
deploy.set_enabled(self.args(), True)
server = self.registered()
self.assertTrue(server["hub"]["enabled"])
self.assertEqual(server["clients"], ["hermes"])
if __name__ == "__main__":
unittest.main()
-131
View File
@@ -1,131 +0,0 @@
from __future__ import annotations
import importlib.util
import json
import pathlib
import sys
import tempfile
import unittest
from unittest import mock
SOURCE = pathlib.Path(__file__).parents[1] / "platform/mcphub/mcphub_git_installer.py"
SPEC = importlib.util.spec_from_file_location("mcphub_git_installer", SOURCE)
assert SPEC and SPEC.loader
installer = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = installer
SPEC.loader.exec_module(installer)
class GitInstallerTest(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.root = pathlib.Path(self.temp.name)
self.appdata = self.root / "appdata"
self.secrets = self.root / "secrets"
self.secrets.mkdir()
self.commits = iter(["a" * 40, "b" * 40, "c" * 40])
def tearDown(self) -> None:
self.temp.cleanup()
def spec(self, **values: object):
data = {
"name": "example",
"repository": "https://github.com/example/mcp",
"ref": "main",
"runtime": "python",
"entrypoint": "example-mcp",
"arguments": ("--stdio",),
"required_env": ("EXAMPLE_TOKEN",),
}
data.update(values)
return installer.GitInstallSpec(**data)
def clone(self, _spec, destination: pathlib.Path) -> str:
destination.mkdir(parents=True, exist_ok=True)
(destination / "pyproject.toml").write_text("[project]\nname='example'\n")
return next(self.commits)
@staticmethod
def clone_same(_spec, destination: pathlib.Path) -> str:
destination.mkdir(parents=True, exist_ok=True)
(destination / "pyproject.toml").write_text("[project]\nname='example'\n")
return "a" * 40
@staticmethod
def build(_source: pathlib.Path, release: pathlib.Path, _entrypoint: str) -> list[str]:
release.mkdir(parents=True)
executable = release / ".venv/bin/example-mcp"
executable.parent.mkdir(parents=True)
executable.write_text("ok")
return [str(executable)]
def test_install_is_disabled_and_reports_only_missing_key_names(self) -> None:
with mock.patch.object(installer, "_clone", self.clone), mock.patch.object(
installer, "_python_release", self.build
):
result = installer.prepare_release(self.spec(), self.appdata, self.secrets)
self.assertFalse(result["config"]["enabled"])
self.assertFalse(result["credentials_ready"])
self.assertEqual(result["missing_env"], ["EXAMPLE_TOKEN"])
self.assertEqual(result["config"]["command"], "/usr/local/bin/run-with-env")
self.assertEqual(result["config"]["args"][-1], "--stdio")
def test_same_release_reuses_build_without_duplicating_arguments(self) -> None:
with mock.patch.object(installer, "_clone", self.clone_same), mock.patch.object(
installer, "_python_release", side_effect=self.build
) as build:
first = installer.prepare_release(self.spec(), self.appdata, self.secrets)
second = installer.prepare_release(self.spec(), self.appdata, self.secrets)
self.assertEqual(build.call_count, 1)
self.assertEqual(first["config"]["args"], second["config"]["args"])
self.assertEqual(second["config"]["args"].count("--stdio"), 1)
def test_update_and_rollback_preserve_both_releases(self) -> None:
with mock.patch.object(installer, "_clone", self.clone), mock.patch.object(
installer, "_python_release", self.build
):
first = installer.prepare_release(self.spec(), self.appdata, self.secrets)
second = installer.prepare_release(self.spec(ref="v2"), self.appdata, self.secrets)
self.assertEqual(second["previous_release"], first["release"])
rolled = installer.rollback_release("example", self.appdata)
self.assertEqual(rolled["release"], first["release"])
self.assertEqual(installer.current_release("example", self.appdata, self.secrets)["release"], first["release"])
def test_failed_update_leaves_previous_state_current(self) -> None:
with mock.patch.object(installer, "_clone", self.clone), mock.patch.object(
installer, "_python_release", self.build
):
first = installer.prepare_release(self.spec(), self.appdata, self.secrets)
with mock.patch.object(installer, "_clone", self.clone), mock.patch.object(
installer, "_python_release", side_effect=installer.GitInstallError("build failed")
):
with self.assertRaises(installer.GitInstallError):
installer.prepare_release(self.spec(ref="broken"), self.appdata, self.secrets)
current = installer.current_release("example", self.appdata, self.secrets)
self.assertEqual(current["release"], first["release"])
def test_registry_updates_only_matching_server(self) -> None:
registry = self.appdata / "config/mcp-registry.json"
registry.parent.mkdir(parents=True)
registry.write_text(json.dumps({"version": 1, "servers": [{"id": "keep", "hermes_id": "keep"}]}))
result = {
"name": "example", "repository": "https://github.com/example/mcp.git",
"requested_ref": "main", "commit": "a" * 40, "release": "a" * 12,
"required_env": [], "secret_file": None,
"config": {"type": "stdio", "command": "example", "args": [], "enabled": False},
}
installer.update_registry(registry, installer.registry_entry(result, "Example"))
servers = json.loads(registry.read_text())["servers"]
self.assertEqual({item["id"] for item in servers}, {"keep", "example-local"})
def test_rejects_non_github_and_escaping_subdirectory(self) -> None:
with self.assertRaises(installer.GitInstallError):
installer.normalize_spec(self.spec(repository="https://evil.example/repo"))
with self.assertRaises(installer.GitInstallError):
installer.normalize_spec(self.spec(subdirectory="../escape"))
if __name__ == "__main__":
unittest.main()
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env python3
"""Regression tests for declarative, update-safe MCPHub settings."""
from __future__ import annotations
import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).parents[1]
SOURCE = ROOT / "platform/mcphub/configure-settings.py"
def load_module():
spec = importlib.util.spec_from_file_location("configure_settings", SOURCE)
module = importlib.util.module_from_spec(spec)
assert spec.loader
spec.loader.exec_module(module)
return module
class MCPHubSettingsTests(unittest.TestCase):
def setUp(self):
self.module = load_module()
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.secrets = self.root / "secrets"
self.secrets.mkdir()
(self.secrets / "remote.env").write_text(
"REMOTE_URL=http://example.test/mcp\nTOKEN=secret-value\n",
encoding="utf-8",
)
self.registry = self.root / "registry.json"
self.registry.write_text(json.dumps({
"version": 1,
"servers": [
{
"id": "remote-local",
"hermes_id": "remote",
"hub": {
"type": "streamable-http",
"secret_file": "remote.env",
"url": "${REMOTE_URL}",
"headers": {"Authorization": "Bearer ${TOKEN}"},
"enabled": True,
},
},
{"id": "client-only", "url": "http://unused/mcp"},
],
}), encoding="utf-8")
def tearDown(self):
self.temp.cleanup()
def test_registry_renders_only_hub_servers_and_expands_secrets(self):
servers = self.module.registry_servers(self.registry, self.secrets, {})
self.assertEqual(list(servers), ["remote"])
self.assertEqual(servers["remote"]["url"], "http://example.test/mcp")
self.assertEqual(
servers["remote"]["headers"]["Authorization"],
"Bearer secret-value",
)
def test_existing_enabled_toggle_survives_reconciliation(self):
servers = self.module.registry_servers(
self.registry, self.secrets, {"remote": {"enabled": False}}
)
self.assertFalse(servers["remote"]["enabled"])
def test_missing_secret_fails_closed(self):
os.unlink(self.secrets / "remote.env")
with self.assertRaises(SystemExit):
self.module.registry_servers(self.registry, self.secrets, {})
def test_hermes_group_is_recoverable_and_bounded(self):
settings = {"groups": [{"id": "keep", "name": "other", "servers": []}]}
self.module.ensure_hermes_group(settings)
self.module.ensure_hermes_group(settings)
groups = settings["groups"]
self.assertEqual(len([group for group in groups if group["name"] == "hermes"]), 1)
hermes = next(group for group in groups if group["name"] == "hermes")
fritzbox = next(item for item in hermes["servers"] if item["name"] == "fritzbox")
self.assertEqual(
fritzbox["tools"],
["list_services", "list_actions", "describe_action", "call_action"],
)
if __name__ == "__main__":
unittest.main()
-616
View File
@@ -1,616 +0,0 @@
#!/usr/bin/env python3
"""Offline tests for the versioned OpenWebUI filters.
The production container provides pydantic. A tiny local stand-in keeps these
logic tests dependency-free and prevents test setup from reaching the network.
"""
from __future__ import annotations
import importlib.util
import json
import sys
import tempfile
import types
import unittest
from pathlib import Path
class _BaseModel:
def __init__(self, **values):
annotations = {}
for base in reversed(type(self).__mro__):
annotations.update(getattr(base, "__annotations__", {}))
for name in annotations:
setattr(self, name, values.get(name, getattr(type(self), name, None)))
fake_pydantic = types.ModuleType("pydantic")
fake_pydantic.BaseModel = _BaseModel
fake_pydantic.Field = lambda default=None, **kwargs: default
sys.modules.setdefault("pydantic", fake_pydantic)
FILTER_DIR = Path(__file__).parents[1] / "platform" / "openwebui" / "filters"
ACTION_DIR = Path(__file__).parents[1] / "platform" / "openwebui" / "actions"
def _load(name: str):
spec = importlib.util.spec_from_file_location(name, FILTER_DIR / f"{name}.py")
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def _load_action(name: str):
spec = importlib.util.spec_from_file_location(name, ACTION_DIR / f"{name}.py")
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
class StabilityGuardTests(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.module = _load("stability_guard")
self.guard = self.module.Filter()
async def test_large_tool_output_is_bounded(self):
body = {
"model": "qwen-fast",
"messages": [
{"role": "user", "content": "Prüfe das Log."},
{"role": "tool", "tool_call_id": "x", "content": "A" * 50000},
],
}
result = await self.guard.inlet(body)
content = result["messages"][1]["content"]
self.assertLessEqual(len(content), self.guard.valves.max_single_tool_chars)
self.assertIn("Werkzeugausgabe gekürzt", content)
async def test_uncensored_uses_its_80k_context_limit(self):
self.assertEqual(
self.guard._context_limit("mikeai-uncensored"),
self.guard.valves.uncensored_context_tokens,
)
async def test_duplicate_calls_disable_tools(self):
call = {
"id": "call",
"type": "function",
"function": {"name": "search", "arguments": '{"q":"same"}'},
}
body = {
"model": "qwen-fast",
"tools": [{"type": "function", "function": {"name": "search"}}],
"tool_ids": ["server:mcp:web"],
"messages": [
{"role": "user", "content": "Suche genau einmal."},
{"role": "assistant", "tool_calls": [call]},
{"role": "tool", "tool_call_id": "1", "content": "nichts"},
{"role": "assistant", "tool_calls": [call]},
{"role": "tool", "tool_call_id": "2", "content": "nichts"},
{"role": "assistant", "tool_calls": [call]},
],
}
result = await self.guard.inlet(body)
self.assertEqual(result["tools"], [])
self.assertEqual(result["tool_ids"], [])
self.assertIn("Weitere Werkzeugaufrufe", result["messages"][0]["content"])
async def test_old_context_is_compacted_before_current_turn(self):
self.guard.valves.default_context_tokens = 10000
self.guard.valves.hard_context_ratio = 0.8
self.guard.valves.reserved_output_tokens = 1000
body = {
"model": "unknown",
"messages": [
{"role": "system", "content": "Sicher arbeiten."},
{"role": "user", "content": "alt " * 15000},
{"role": "assistant", "content": "altantwort " * 8000},
{"role": "tool", "tool_call_id": "old", "content": "log " * 20000},
{"role": "user", "content": "Aktuelle wichtige Frage"},
],
}
result = await self.guard.inlet(body)
self.assertEqual(result["messages"][-1]["content"], "Aktuelle wichtige Frage")
self.assertLess(len(result["messages"][1]["content"]), 3000)
async def test_private_csv_disables_web_and_requires_local_table_analysis(self):
body = {
"model": "qwen-fast",
"features": {"web_search": True, "code_interpreter": True},
"tool_ids": ["server:mcp:web-local", "server:mcp:arr-local"],
"tools": [
{"type": "function", "function": {"name": "search_web"}},
{"type": "function", "function": {"name": "execute_code"}},
],
"metadata": {"files": [{"name": "private-bank.csv"}]},
"messages": [
{"role": "user", "content": "Sortiere Ein- und Ausgänge."},
],
}
result = await self.guard.inlet(body)
self.assertEqual(result["tool_ids"], [])
self.assertFalse(result["features"]["web_search"])
self.assertTrue(result["features"]["code_interpreter"])
self.assertEqual(
[tool["function"]["name"] for tool in result["tools"]],
["execute_code"],
)
self.assertIn("private table rule", result["messages"][0]["content"])
self.assertIn("sep=None", result["messages"][0]["content"])
self.assertIn("delimiter", result["messages"][0]["content"])
async def test_private_csv_is_detected_from_user_text_without_metadata(self):
body = {
"model": "qwen-fast",
"tools": [
{"type": "function", "function": {"name": "search_web"}},
{"type": "function", "function": {"name": "execute_code"}},
],
"messages": [
{"role": "user", "content": "Werte bitte diese CSV meines Bankkontos aus."},
],
}
result = await self.guard.inlet(body)
self.assertEqual(
[tool["function"]["name"] for tool in result["tools"]],
["execute_code"],
)
async def test_private_csv_stops_before_openwebui_hard_tool_limit(self):
calls = []
for index in range(8):
calls.extend(
[
{
"role": "assistant",
"tool_calls": [
{
"function": {
"name": "execute_code",
"arguments": '{"code":"step %d"}' % index,
}
}
],
},
{"role": "tool", "content": "ok", "tool_call_id": str(index)},
]
)
body = {
"model": "qwen-fast",
"tools": [
{"type": "function", "function": {"name": "execute_code"}},
],
"messages": [
{"role": "user", "content": "Werte diese CSV aus."},
*calls,
],
}
result = await self.guard.inlet(body)
self.assertEqual(result["tools"], [])
self.assertIn("vorhandenen Ergebnisse", result["messages"][0]["content"])
class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.module = _load("auto_tool_selector")
self.selector = self.module.Filter()
async def _select(self, prompt: str, existing=None):
body = {
"model": "mikeai-medium",
"messages": [{"role": "user", "content": prompt}],
}
if existing is not None:
body["tool_ids"] = existing
return await self.selector.inlet(body)
async def test_homeassistant_is_selected_for_room_temperature(self):
result = await self._select("Wie warm ist es gerade in der Küche?")
self.assertEqual(
result["tool_ids"], ["server:mcp:homeassistant-local"]
)
self.assertIn("Never call ha_list_states merely", result["messages"][0]["content"])
self.assertIn("Never infer an automation entity_id", result["messages"][0]["content"])
async def test_hyphenated_homeassistant_and_tool_name_are_selected(self):
result = await self._select(
"Führe einen Home-Assistant-Test aus und nutze ha_list_states genau einmal."
)
self.assertEqual(result["tool_ids"], ["server:mcp:homeassistant-local"])
async def test_general_web_is_available_without_site_specific_rules(self):
result = await self._select("Erkläre mir kurz, wie ein Fahrrad funktioniert.")
self.assertTrue(result["features"]["web_search"])
self.assertTrue(result["metadata"]["features"]["web_search"])
self.assertNotIn("tool_ids", result)
async def test_weather_gets_native_web_and_compact_fallback(self):
result = await self._select("Soll es heute in Rastatt regnen?")
self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
async def test_youtube_channel_question_gets_general_web_fallback(self):
result = await self._select(
"Welches Video steht aktuell oben auf dem YouTube-Kanal The Proper People?"
)
self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
async def test_unraid_uses_readonly_not_mua(self):
result = await self._select(
"Welche Docker-Container laufen aktuell auf Unraid?"
)
self.assertEqual(
result["tool_ids"], ["server:mcp:mua-readonly-local"]
)
self.assertNotIn("server:mcp:mua", result["tool_ids"])
self.assertIn("bounded evidence ladder", result["messages"][0]["content"])
self.assertIn("Do not dump complete configuration files", result["messages"][0]["content"])
async def test_explicit_unraid_update_gets_read_and_management_tools(self):
result = await self._select(
"Prüfe auf Unraid alle Docker-Updates, führe die Updates durch und kontrolliere danach den Zustand."
)
self.assertEqual(
result["tool_ids"],
["server:mcp:mua-readonly-local", "server:mcp:mua"],
)
self.assertIn("single batched update workflow", result["messages"][0]["content"])
async def test_unraid_update_question_stays_readonly(self):
result = await self._select(
"Gibt es auf Unraid Updates für Docker-Container? Bitte nur prüfen."
)
self.assertEqual(
result["tool_ids"], ["server:mcp:mua-readonly-local"]
)
async def test_readonly_unraid_media_audit_does_not_attach_admin_or_arr(self):
result = await self._select(
"Prüfe auf Unraid, welche Folgen meiner Hörspielserie Die drei Fragezeichen fehlen. "
"Nur lesen, ohne Downloads, Umbenennungen oder sonstige Änderungen."
)
self.assertEqual(
result["tool_ids"], ["server:mcp:mua-readonly-local"]
)
self.assertIn("unraid_files_inventory", result["messages"][0]["content"])
async def test_unraid_media_audit_with_deezer_enables_native_web(self):
result = await self._select(
"Prüfe auf Unraid, welche Folgen meiner Hörspielserie fehlen, ermittle die "
"aktuelle offizielle Liste online und prüfe jede fehlende Folge bei Deezer. "
"Nur lesen, ohne Downloads oder Änderungen."
)
self.assertEqual(
result["tool_ids"],
["server:mcp:mua-readonly-local", "server:mcp:web-general-local"],
)
self.assertTrue(result["features"]["web_search"])
self.assertTrue(result["metadata"]["features"]["web_search"])
self.assertIn("search_web/fetch_url", result["messages"][0]["content"])
self.assertEqual(result["reasoning_effort"], "medium")
async def test_unraid_media_write_gets_portable_operator_and_web(self):
result = await self._select(
"Ermittle online das neueste Video, lade es mit yt-dlp auf dem Unraid-Host "
"in einen temporären Ordner, konvertiere es mit ffmpeg und lege die fertige "
"Datei im Share Transfer ab."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
"server:mcp:mua",
"server:mcp:web-general-local",
],
)
self.assertIn("direct MUA management or shell tool", result["messages"][0]["content"])
self.assertIn("Do not ask the user to enable another tool", result["messages"][0]["content"])
self.assertIn("start-status-result pattern", result["messages"][0]["content"])
self.assertIn("transient-by-default dependency handling", result["messages"][0]["content"])
self.assertIn("task-local copy under /tmp", result["messages"][0]["content"])
self.assertTrue(result["metadata"]["mikeai_long_operator_task"])
async def test_generic_remote_host_file_operation_gets_operator(self):
result = await self._select(
"Führe auf dem Server ein vorhandenes Skript aus und speichere die neue Datei unter /data/export."
)
self.assertEqual(result["tool_ids"], ["server:mcp:athena-operator-local"])
self.assertTrue(result["metadata"]["mikeai_long_operator_task"])
async def test_readonly_diagnostics_do_not_get_long_operator_budget(self):
result = await self._select("Prüfe nur lesend den Zustand von Unraid.")
self.assertNotIn("mikeai_long_operator_task", result["metadata"])
async def test_narrow_safety_clause_does_not_cancel_authorized_unraid_write(self):
result = await self._select(
"Lade das Video auf dem Unraid-Host herunter, konvertiere es und lege es "
"im Transfer-Share ab. Installiere dabei kein Paket dauerhaft und ändere "
"keine Unraid-Systemkonfiguration."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
"server:mcp:mua",
],
)
async def test_plain_download_advice_does_not_attach_operator(self):
result = await self._select("Erkläre mir, wie ein Browser einen Download technisch durchführt.")
self.assertNotIn("tool_ids", result)
async def test_voice_transcription_variants_select_unraid(self):
result = await self._select(
"Welche Dacher Contäner laufen aktuell auf dem Anrate Server?"
)
self.assertEqual(
result["tool_ids"], ["server:mcp:mua-readonly-local"]
)
async def test_navidrome_is_selected(self):
result = await self._select(
"Schau in Navidrome nach ähnlichen Titeln und meiner Playlist."
)
self.assertEqual(result["tool_ids"], ["server:mcp:navidrome-local"])
async def test_platform_context_is_selected(self):
result = await self._select(
"Wie ist der KI-Host Athena aufgebaut und wo liegt der Recovery-Koffer?"
)
self.assertEqual(result["tool_ids"], ["server:mcp:athena-operator-local"])
async def test_athena_operator_is_selected_for_platform_work(self):
result = await self._select(
"Baue und deploye auf Athena einen neuen MCP-Container."
)
self.assertEqual(
result["tool_ids"], ["server:mcp:athena-operator-local"]
)
async def test_mcp_build_from_github_gets_source_and_operator(self):
result = await self._select(
"Ich möchte hierfür einen MCP bauen: https://github.com/foo/bar"
)
self.assertEqual(
result["tool_ids"],
["server:mcp:github-local", "server:mcp:athena-operator-local"],
)
async def test_existing_unraid_backend_gets_operator_and_runtime_evidence(self):
result = await self._select(
"Baue aus https://github.com/foo/deemix einen MCP. Deemix läuft bereits als Container auf Unraid; prüfe ihn zuerst."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:github-local",
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
],
)
self.assertIn("integrate or relay", result["messages"][0]["content"])
async def test_readonly_integration_plan_does_not_attach_unraid_admin(self):
result = await self._select(
"Analysiere https://github.com/foo/deemix und prüfe den auf Unraid "
"laufenden Container und entwirf einen Athena-MCP. Erstelle keinen zweiten Container, ändere nichts "
"und lies keine Secrets."
)
self.assertEqual(
result["tool_ids"],
[
"server:mcp:github-local",
"server:mcp:athena-operator-local",
"server:mcp:mua-readonly-local",
],
)
async def test_github_and_explicit_web_adds_compact_general_web(self):
result = await self._select(
"Prüfe dieses GitHub Repository und suche zusätzlich im Netz nach Nutzerstimmen."
)
self.assertEqual(
result["tool_ids"],
["server:mcp:github-local", "server:mcp:web-general-local"],
)
async def test_plain_public_web_request_attaches_general_not_legacy_web(self):
result = await self._select(
"Suche im Netz auf MakerWorld einen Schlümpfe-Schlüsselanhänger."
)
self.assertNotIn("server:mcp:web-local", result.get("tool_ids", []))
self.assertIn("server:mcp:web-general-local", result.get("tool_ids", []))
async def test_ebay_research_gets_general_marketplace_protocol(self):
result = await self._select(
"Suche auf eBay nach einem vollständigen Highscreen 386 PC mit Preis und Versand."
)
self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
self.assertIn("Marketplace research protocol", result["messages"][0]["content"])
self.assertIn("roughly three searches and five page fetches", result["messages"][0]["content"])
self.assertTrue(result["metadata"]["mikeai_marketplace_research"])
async def test_generic_marketplace_research_does_not_need_site_rule(self):
result = await self._select(
"Finde auf einem Marktplatz aktuelle Angebote für einen gebrauchten Synthesizer."
)
self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"])
self.assertIn("deduplicate by listing URL or item number", result["messages"][0]["content"])
self.assertTrue(result["metadata"]["mikeai_marketplace_research"])
async def test_manual_tool_is_preserved(self):
result = await self._select(
"Prüfe Sonarr.", ["server:mcp:manually-selected"]
)
self.assertEqual(
result["tool_ids"],
["server:mcp:manually-selected", "server:mcp:arr-local"],
)
async def test_plain_chat_gets_no_tools(self):
result = await self._select("Erkläre mir den Unterschied zwischen RAM und SSD.")
self.assertNotIn("tool_ids", result)
async def test_selection_adds_write_safety_rule(self):
result = await self._select("Zeige mir die Home Assistant Automatisierungen.")
self.assertEqual(result["messages"][0]["role"], "system")
self.assertIn("Availability is not authorization", result["messages"][0]["content"])
class MetricsTests(unittest.IsolatedAsyncioTestCase):
async def test_metrics_file_contains_no_chat_content_or_ids(self):
module = _load("local_performance_metrics")
metrics = module.Filter()
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "metrics.jsonl"
metrics.valves.metrics_path = str(path)
metadata = {"message_id": "secret-message-id", "chat_id": "secret-chat-id"}
await metrics.inlet(
{
"model": "qwen-fast",
"messages": [{"role": "user", "content": "private prompt"}],
"tools": [{"name": "tool"}],
},
__metadata__=metadata,
)
await metrics.outlet(
{
"model": "qwen-fast",
"messages": [
{
"role": "assistant",
"content": "private answer",
"usage": {
"prompt_tokens": 10,
"completion_tokens": 4,
"total_tokens": 14,
},
}
],
},
__metadata__=metadata,
)
raw = path.read_text()
record = json.loads(raw)
self.assertEqual(record["prompt_tokens"], 10)
self.assertNotIn("private", raw)
self.assertNotIn("secret", raw)
class SecretRedactionTests(unittest.IsolatedAsyncioTestCase):
async def test_only_tool_and_assistant_content_is_redacted(self):
module = _load("secret_redaction")
guard = module.Filter()
token = "eyJ" + "A" * 24 + "." + "B" * 24 + "." + "C" * 16
body = {
"messages": [
{"role": "user", "content": f"Absichtlich lokal nutzen: {token}"},
{"role": "tool", "content": f'{{"api_key":"1234567890abcdef"}} {token}'},
]
}
result = await guard.inlet(body)
self.assertIn(token, result["messages"][0]["content"])
self.assertNotIn(token, result["messages"][1]["content"])
self.assertNotIn("1234567890abcdef", result["messages"][1]["content"])
outlet = {
"messages": [
{"role": "assistant", "content": "Bearer " + "abcdefghijklmnopqrstuvwxyz"}
]
}
result = await guard.outlet(outlet)
self.assertNotIn("abcdefghijklmnopqrstuvwxyz", result["messages"][0]["content"])
async def test_documentation_placeholders_and_paths_are_not_redacted(self):
module = _load("secret_redaction")
guard = module.Filter()
content = "\n".join(
[
'ROUTER_API_KEY=${ROUTER_API_KEY:?required}',
'API_KEY=/etc/mike-ai/router-api-key',
'access_token=<YOUR_TOKEN>',
'password=[REDACTED]',
'API-Key: Inhalt von /etc/mike-ai/router-api-key',
]
)
redacted, counts = guard._redact(content)
self.assertEqual(redacted, content)
self.assertEqual(counts, {})
async def test_notification_reports_category_and_origin_but_not_value(self):
module = _load("secret_redaction")
guard = module.Filter()
events = []
async def emit(event):
events.append(event)
secret = "realistic-secret-value-123456"
body = {"messages": [{"role": "tool", "content": f"api_key={secret}"}]}
result = await guard.inlet(body, __event_emitter__=emit)
self.assertNotIn(secret, result["messages"][0]["content"])
description = events[0]["data"]["description"]
self.assertIn("API-Key: 1", description)
self.assertIn("Werkzeugausgaben", description)
self.assertIn("nicht protokolliert", description)
self.assertNotIn(secret, description)
class QuickActionTests(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.module = _load_action("quick_actions")
self.action = self.module.Action()
self.body = {
"id": "assistant-1",
"model": "qwen-fast",
"messages": [
{"id": "user-1", "role": "user", "content": "Frage"},
{"id": "assistant-1", "role": "assistant", "content": "Lange Antwort"},
],
}
async def test_summary_appends_to_selected_message(self):
async def fake_completion(*args, **kwargs):
return "- Kurze Antwort"
self.action._completion = fake_completion
result = await self.action.action(self.body, "summary")
self.assertEqual(result["messages"][0]["id"], "assistant-1")
self.assertIn("### Kurzfassung", result["messages"][0]["content"])
self.assertIn("Kurze Antwort", result["messages"][0]["content"])
async def test_source_check_uses_web_evidence(self):
completions = iter(["Qwen Fakten", "Bestätigt: Aussage [https://example.invalid]"])
async def fake_completion(*args, **kwargs):
return next(completions)
async def fake_web(query):
self.assertEqual(query, "Qwen Fakten")
return '{"sources":[{"url":"https://example.invalid"}]}'
self.action._completion = fake_completion
self.action._web_research = fake_web
result = await self.action.action(self.body, "sources")
self.assertIn("### Quellenprüfung", result["messages"][0]["content"])
self.assertIn("example.invalid", result["messages"][0]["content"])
async def test_markdown_copy_uses_browser_clipboard(self):
calls = []
async def event_call(event):
calls.append(event)
return True
await self.action.action(self.body, "copy_markdown", __event_call__=event_call)
self.assertEqual(calls[0]["type"], "execute")
self.assertIn("navigator.clipboard.writeText", calls[0]["data"]["code"])
self.assertIn("Lange Antwort", calls[0]["data"]["code"])
if __name__ == "__main__":
unittest.main()
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env python3
"""Focused offline tests for the model-oriented Radarr overlay."""
from __future__ import annotations
import importlib.util
import sys
import types
import unittest
from pathlib import Path
SOURCE = Path(__file__).parents[1] / "platform/mcp/patches/mcp_radarr.py"
def load_module():
fastmcp = types.ModuleType("fastmcp")
fastmcp.FastMCP = object
sys.modules["fastmcp"] = fastmcp
pydantic = types.ModuleType("pydantic")
pydantic.Field = lambda *args, **kwargs: kwargs.get("default")
sys.modules["pydantic"] = pydantic
auth = types.ModuleType("arr_mcp.auth")
auth.get_radarr_client = lambda: None
sys.modules["arr_mcp"] = types.ModuleType("arr_mcp")
sys.modules["arr_mcp.auth"] = auth
spec = importlib.util.spec_from_file_location("radarr_patch", SOURCE)
module = importlib.util.module_from_spec(spec)
assert spec.loader
spec.loader.exec_module(module)
return module
class RadarrPatchTests(unittest.TestCase):
def setUp(self):
self.module = load_module()
self.movies = [{
"id": 12, "title": "Example", "year": 2024, "hasFile": True,
"alternateTitles": [{"title": "large unwanted block"}],
"movieFile": {
"id": 44, "relativePath": "Example.mkv", "size": 2147483648,
"quality": {"quality": {"name": "Bluray-1080p"}},
"mediaInfo": {
"videoCodec": "x264", "resolution": "1920x1080",
"videoBitDepth": 8, "audioCodec": "EAC3",
"audioLanguages": "ger/eng", "subtitles": "ger",
},
},
}]
def test_inventory_is_compact_and_alias_aware(self):
result = self.module._compact_inventory(self.movies, codecs="h264")
self.assertEqual(result["totalMatched"], 1)
self.assertEqual(result["movies"][0]["videoCodec"], "x264")
self.assertEqual(result["movies"][0]["sizeGiB"], 2.0)
self.assertNotIn("alternateTitles", result["movies"][0])
def test_filter_and_pagination_return_valid_bounded_data(self):
result = self.module._compact_inventory(self.movies * 5, query="example", offset=1, limit=2)
self.assertEqual(result["returned"], 2)
self.assertTrue(result["hasMore"])
def test_surface_has_only_explicit_read_tools(self):
class FakeMcp:
def __init__(self):
self.names = []
def tool(self, **_kwargs):
def decorate(function):
self.names.append(function.__name__)
return function
return decorate
mcp = FakeMcp()
self.module.register_radarr_tools(mcp)
self.assertEqual(
mcp.names,
["radarr_find_movie", "radarr_movie_codec_inventory", "radarr_search_releases"],
)
self.assertNotIn("radarr_action", mcp.names)
if __name__ == "__main__":
unittest.main()
-195
View File
@@ -1,195 +0,0 @@
#!/usr/bin/env python3
"""Focused offline tests for the model-oriented Sonarr overlay."""
from __future__ import annotations
import importlib.util
import sys
import types
import unittest
from pathlib import Path
from unittest.mock import patch
SOURCE = Path(__file__).parents[1] / "platform/mcp/patches/mcp_sonarr.py"
def load_module():
async def run_blocking(function, *args, **kwargs):
kwargs.pop("service", None)
return function(*args, **kwargs)
def dispatch(client, action, kwargs, **_options):
return getattr(client, action)(**kwargs)
utilities = types.ModuleType("agent_utilities.mcp_utilities")
utilities.dispatch = dispatch
utilities.run_blocking = run_blocking
sys.modules["agent_utilities"] = types.ModuleType("agent_utilities")
sys.modules["agent_utilities.mcp_utilities"] = utilities
fastmcp = types.ModuleType("fastmcp")
fastmcp.FastMCP = object
sys.modules["fastmcp"] = fastmcp
pydantic = types.ModuleType("pydantic")
pydantic.Field = lambda *args, **kwargs: kwargs.get("default")
sys.modules["pydantic"] = pydantic
auth = types.ModuleType("arr_mcp.auth")
auth.get_sonarr_client = lambda: None
sys.modules["arr_mcp"] = types.ModuleType("arr_mcp")
sys.modules["arr_mcp.auth"] = auth
spec = importlib.util.spec_from_file_location("sonarr_patch", SOURCE)
module = importlib.util.module_from_spec(spec)
assert spec.loader
spec.loader.exec_module(module)
return module
class FakeSonarrClient:
def __init__(self, *, rejected: bool = False) -> None:
self.posted = []
self.release = {
"guid": "exact-guid",
"title": "Murder.She.Wrote.S07.German.AC3D.DL.1080p.WebHD.x265-FuN",
"indexer": "Test Indexer",
"indexerId": 7,
"size": 27_600_000_000,
"protocol": "usenet",
"downloadAllowed": not rejected,
"releaseGroup": "FuN",
"seasonNumber": 7,
"fullSeason": True,
"rejections": ["Existing file has equal or better quality"] if rejected else [],
}
def get_release(self, **_kwargs):
return [dict(self.release)]
def get_episode(self, **_kwargs):
return [
{"id": 1, "seasonNumber": 7, "episodeNumber": 1, "hasFile": True},
{"id": 2, "seasonNumber": 7, "episodeNumber": 2, "hasFile": False},
]
def post_release(self, data=None, **kwargs):
payload = data if data is not None else kwargs
self.posted.append(payload)
return payload
class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self.module = load_module()
self.module._APPROVALS.clear()
async def test_exact_release_requires_preview_and_ticket(self) -> None:
client = FakeSonarrClient()
scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"}
preview = await self.module._preview_release_grab(client, scope)
self.assertFalse(preview["download_started"])
self.assertEqual(preview["existing_episode_files_in_season"], 1)
self.assertTrue(preview["approval_ticket"])
result = await self.module._grab_release(
client,
{**scope, "confirm": True, "approval_ticket": preview["approval_ticket"]},
)
self.assertTrue(result["download_started"])
self.assertFalse(result["replacement_guaranteed"])
self.assertEqual(len(client.posted), 1)
self.assertEqual(client.posted[0]["guid"], "exact-guid")
async def test_rejected_release_needs_force_in_preview(self) -> None:
client = FakeSonarrClient(rejected=True)
scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"}
blocked = await self.module._preview_release_grab(client, scope)
self.assertTrue(blocked["force_required"])
self.assertIsNone(blocked["approval_ticket"])
approved = await self.module._preview_release_grab(client, {**scope, "force": True})
self.assertFalse(approved["force_required"])
self.assertTrue(approved["approval_ticket"])
async def test_guid_must_still_match_current_sonarr_results(self) -> None:
client = FakeSonarrClient()
with self.assertRaisesRegex(ValueError, "no longer present"):
await self.module._preview_release_grab(
client,
{"series_id": 42, "season_number": 7, "guid": "different-guid"},
)
async def test_search_can_limit_results_to_group_and_season_pack(self) -> None:
client = FakeSonarrClient()
client.get_release = lambda **_kwargs: [
dict(client.release),
{
**client.release,
"guid": "episode-guid",
"title": "Mord.ist.ihr.Hobby.S07E02.German.1080p-FuN",
"fullSeason": False,
},
{
**client.release,
"guid": "other-group",
"title": "Mord.ist.ihr.Hobby.S07.German.1080p-HQC",
"releaseGroup": "HQC",
},
]
result = await self.module._search_releases(
client,
{
"series_id": 42,
"season_number": 7,
"release_group": "FuN",
"season_pack_only": True,
},
)
self.assertEqual(result["results"]["total"], 1)
self.assertEqual(result["results"]["items"][0]["guid"], "exact-guid")
def test_read_only_surface_is_explicit_and_has_no_generic_action(self) -> None:
class FakeMcp:
def __init__(self):
self.names = []
def tool(self, **_kwargs):
def decorate(function):
self.names.append(function.__name__)
return function
return decorate
with patch.dict("os.environ", {"ARR_MCP_WRITE": "0"}):
mcp = FakeMcp()
self.module.register_sonarr_tools(mcp)
self.assertEqual(
mcp.names,
[
"sonarr_find_series",
"sonarr_get_season_summary",
"sonarr_search_releases",
"sonarr_system_status",
],
)
self.assertNotIn("sonarr_action", mcp.names)
def test_write_tools_are_registered_only_when_enabled(self) -> None:
class FakeMcp:
def __init__(self):
self.names = []
def tool(self, **_kwargs):
def decorate(function):
self.names.append(function.__name__)
return function
return decorate
with patch.dict("os.environ", {"ARR_MCP_WRITE": "1"}):
mcp = FakeMcp()
self.module.register_sonarr_tools(mcp)
self.assertIn("sonarr_preview_release_grab", mcp.names)
self.assertIn("sonarr_grab_release", mcp.names)
self.assertIn("sonarr_preview_episode_search", mcp.names)
self.assertIn("sonarr_start_episode_search", mcp.names)
if __name__ == "__main__":
unittest.main()
-148
View File
@@ -1,148 +0,0 @@
#!/usr/bin/env python3
"""Regression tests for the compact web MCP facade."""
from __future__ import annotations
import importlib.util
import json
import pathlib
import unittest
from unittest import mock
ROOT = pathlib.Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"web_search_mcp", ROOT / "platform/web-search/web_search_mcp.py"
)
WEB = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(WEB)
class WebSearchMcpTests(unittest.TestCase):
def setUp(self) -> None:
WEB._search_attempts.clear()
def test_tool_surface_stays_small_and_explicit(self) -> None:
self.assertEqual(
[tool["name"] for tool in WEB.TOOLS],
["web_search", "web_read", "web_youtube", "web_compare", "web_shop", "web_research"],
)
def test_current_queries_do_not_get_wikipedia_noise(self) -> None:
with (
mock.patch.object(WEB, "SEARXNG_URL", "http://searxng:8080"),
mock.patch.object(WEB, "searxng_json", return_value={"results": []}),
mock.patch.object(WEB, "wikipedia_search") as wikipedia,
):
results, _ = WEB.general_discovery("latest video The Proper People", 4)
self.assertEqual(results, [])
wikipedia.assert_not_called()
def test_empty_fresh_search_relaxes_once_and_marks_result(self) -> None:
hit = {"title": "Current page", "url": "https://example.com/current"}
with mock.patch.object(
WEB,
"general_discovery",
side_effect=[([], []), ([hit], [])],
) as discovery:
result = WEB.web_search({
"query": "current test release",
"freshness": "week",
"max_results": 3,
})
self.assertTrue(result["task_complete"])
self.assertFalse(result["freshness_applied"])
self.assertIn("unfiltered", result["backend_warning"])
self.assertEqual(discovery.call_count, 2)
def test_related_search_budget_is_enforced(self) -> None:
with mock.patch.object(WEB, "SEARCH_BUDGET_MAX_RELATED_CALLS", 2):
self.assertTrue(WEB.consume_search_budget("latest Proper People video")[0])
self.assertTrue(WEB.consume_search_budget("Proper People newest video")[0])
self.assertFalse(WEB.consume_search_budget("newest video by Proper People")[0])
def test_youtube_feed_provides_order_and_dates(self) -> None:
feed = b'''<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015"
xmlns="http://www.w3.org/2005/Atom">
<entry><yt:videoId>new123</yt:videoId><title>Newest</title>
<published>2026-08-23T12:00:00+00:00</published>
<author><name>The Proper People</name></author></entry>
<entry><yt:videoId>old456</yt:videoId><title>Older</title>
<published>2026-08-10T12:00:00+00:00</published>
<author><name>The Proper People</name></author></entry>
</feed>'''
with mock.patch.object(WEB, "fetch_public_bytes", return_value=feed):
rows = WEB.youtube_feed_records(
"https://www.youtube.com/channel/UCcem9I78ybZLHLRUlkUO3sw", 2
)
self.assertEqual([row["title"] for row in rows], ["Newest", "Older"])
self.assertEqual(rows[0]["published_at"], "2026-08-23T12:00:00+00:00")
def test_latest_youtube_is_one_bounded_specialist_operation(self) -> None:
row = {
"title": "Newest",
"url": "https://www.youtube.com/watch?v=new123",
"source_kind": "youtube_channel_feed",
}
with (
mock.patch.object(WEB, "resolve_youtube_channel", return_value="https://www.youtube.com/channel/UCcem9I78ybZLHLRUlkUO3sw"),
mock.patch.object(WEB, "youtube_feed_records", return_value=[row]),
mock.patch.object(WEB, "run_ytdlp") as ytdlp,
):
result = WEB.web_youtube({"query": "The Proper People", "mode": "latest"})
self.assertTrue(result["task_complete"])
self.assertEqual(result["results"][0]["title"], "Newest")
ytdlp.assert_not_called()
def test_latest_long_youtube_uses_verified_videos_tab(self) -> None:
row = {
"title": "Newest long video",
"url": "https://www.youtube.com/watch?v=long123",
"content_type": "long",
"content_type_verified": True,
"source_kind": "youtube_videos_tab",
}
with (
mock.patch.object(
WEB,
"resolve_youtube_channel",
return_value="https://www.youtube.com/channel/UCcem9I78ybZLHLRUlkUO3sw",
),
mock.patch.object(WEB, "youtube_tab_records", return_value=[row]) as tab,
mock.patch.object(WEB, "youtube_feed_records") as feed,
):
result = WEB.web_youtube({
"query": "The Proper People",
"mode": "latest",
"content_type": "long",
})
tab.assert_called_once_with(
"https://www.youtube.com/channel/UCcem9I78ybZLHLRUlkUO3sw", "long", 5
)
feed.assert_not_called()
self.assertEqual(result["content_type_filter"], "long")
self.assertTrue(result["results"][0]["content_type_verified"])
def test_youtube_rejects_content_filter_outside_latest_mode(self) -> None:
with self.assertRaisesRegex(ValueError, "only supported with mode=latest"):
WEB.web_youtube({
"query": "The Proper People",
"mode": "search",
"content_type": "long",
})
def test_web_read_does_not_consume_search_loop_budget(self) -> None:
page = {"url": "https://example.com/a", "page_evidence": ["Evidence"]}
with mock.patch.object(WEB, "scrape", return_value=[page]):
result = json.loads(WEB.call_tool("web_read", {
"url": "https://example.com/a",
"question": "What does this page say?",
}))
self.assertTrue(result["task_complete"])
self.assertEqual(WEB._search_attempts, [])
if __name__ == "__main__":
unittest.main()