Files
Owner/tests/test_external_adapters.py
T

117 lines
5.2 KiB
Python

from __future__ import annotations
import unittest
from unittest.mock import patch
from owner_voice_pet.config import AppConfig
from owner_voice_pet.external_adapters import (
CODEX_COMPUTER_USE_SAFETY_REFERENCE,
BrowserPlaywrightAdapter,
ComputerControlProvider,
OpenInterpreterAdapter,
is_high_risk_task,
planned_external_adapters,
)
from owner_voice_pet.tool_router import ToolCallRequest, ToolContext, ToolRouter
class ExternalAdaptersTests(unittest.TestCase):
def test_openinterpreter_disabled_returns_unavailable_status(self) -> None:
adapter = OpenInterpreterAdapter(enabled=False)
request = ToolCallRequest("1", "openinterpreter.run", {"task": "列出文件"}, "turn-1")
result = adapter.execute(request, ToolContext())
self.assertEqual(result.status, "failed")
self.assertEqual(result.error_code, "OPENINTERPRETER_DISABLED")
def test_openinterpreter_missing_command_is_structured(self) -> None:
adapter = OpenInterpreterAdapter(enabled=True, command="/tmp/owner-missing-openinterpreter")
request = ToolCallRequest("1", "openinterpreter.run", {"task": "列出文件"}, "turn-1")
result = adapter.execute(request, ToolContext())
self.assertEqual(result.status, "failed")
self.assertEqual(result.error_code, "OPENINTERPRETER_UNAVAILABLE")
def test_openinterpreter_low_risk_dry_run(self) -> None:
adapter = OpenInterpreterAdapter(enabled=True, command="python3", dry_run=True)
request = ToolCallRequest("1", "openinterpreter.run", {"task": "只读检查当前目录"}, "turn-1")
result = adapter.execute(request, ToolContext())
self.assertEqual(result.status, "success")
self.assertIn("dry-run Open Interpreter", result.output_text)
def test_openinterpreter_high_risk_task_requires_confirmation(self) -> None:
adapter = OpenInterpreterAdapter(enabled=True, command="python3", dry_run=True)
request = ToolCallRequest("1", "openinterpreter.run", {"task": "delete files"}, "turn-1")
result = adapter.execute(request, ToolContext())
self.assertEqual(result.status, "confirmation_required")
self.assertEqual(result.error_code, "OPENINTERPRETER_HIGH_RISK")
def test_playwright_disabled_and_missing_dependency_are_structured(self) -> None:
request = ToolCallRequest("1", "browser.playwright", {"task": "读取网页标题"}, "turn-1")
disabled = BrowserPlaywrightAdapter(enabled=False).execute(request, ToolContext())
with patch("importlib.util.find_spec", return_value=None):
missing = BrowserPlaywrightAdapter(enabled=True).execute(request, ToolContext())
self.assertEqual(disabled.error_code, "PLAYWRIGHT_DISABLED")
self.assertEqual(missing.error_code, "PLAYWRIGHT_UNAVAILABLE")
def test_playwright_high_risk_task_requires_confirmation_when_available(self) -> None:
request = ToolCallRequest("1", "browser.playwright", {"task": "purchase item"}, "turn-1")
with patch("importlib.util.find_spec", return_value=object()):
result = BrowserPlaywrightAdapter(enabled=True).execute(request, ToolContext())
self.assertEqual(result.status, "confirmation_required")
self.assertEqual(result.error_code, "PLAYWRIGHT_SENSITIVE_ACTION")
def test_computer_control_provider_is_reserved_and_unsupported(self) -> None:
result = ComputerControlProvider(enabled=True).execute(
ToolCallRequest("1", "computer.control", {"action": "click"}, "turn-1"),
ToolContext(),
)
self.assertEqual(result.status, "failed")
self.assertEqual(result.error_code, "COMPUTER_CONTROL_UNSUPPORTED")
def test_planned_external_adapters_respect_config_defaults(self) -> None:
adapters = planned_external_adapters(AppConfig())
self.assertFalse(adapters["openinterpreter.run"].enabled)
self.assertFalse(adapters["browser.playwright"].enabled)
self.assertFalse(adapters["computer.control"].enabled)
def test_tool_router_requires_confirmation_for_browser_sensitive_intent(self) -> None:
router = ToolRouter({"browser.playwright": BrowserPlaywrightAdapter(enabled=True)})
request = ToolCallRequest(
"1",
"browser.playwright",
{"task": "读取网页"},
"turn-1",
natural_language_intent="购买商品",
)
decision = router.route(request, ToolContext())
self.assertEqual(decision.action, "require_confirmation")
self.assertEqual(decision.risk_level, "high")
def test_codex_computer_use_reference_is_policy_only(self) -> None:
self.assertIn("safety-confirmation", CODEX_COMPUTER_USE_SAFETY_REFERENCE)
self.assertIn("public", CODEX_COMPUTER_USE_SAFETY_REFERENCE)
self.assertNotIn("private API", CODEX_COMPUTER_USE_SAFETY_REFERENCE)
def test_high_risk_task_classifier_covers_chinese_and_english(self) -> None:
self.assertTrue(is_high_risk_task("删除下载目录"))
self.assertTrue(is_high_risk_task("upload account backup"))
self.assertFalse(is_high_risk_task("read local project summary"))
if __name__ == "__main__":
unittest.main()