File size: 7,740 Bytes
068bc7f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | """Unit tests for Stack 2.9 tools."""
import datetime
import json
import os
import tempfile
import pytest
# Patch data dirs before importing tools
_temp_data_dir = tempfile.mkdtemp()
def _patch_data_dir(path: str):
import src.tools.web_search as ws
import src.tools.task_management as tm
import src.tools.scheduling as sc
ws.DATA_DIR = path
ws.TASKS_FILE = os.path.join(path, "tasks.json")
ws.CACHE_FILE = os.path.join(path, "web_search_cache.json")
tm.DATA_DIR = path
tm.TASKS_FILE = os.path.join(path, "tasks.json")
sc.DATA_DIR = path
sc.SCHEDULES_FILE = os.path.join(path, "schedules.json")
_patch_data_dir(_temp_data_dir)
from src.tools.base import BaseTool, ToolResult
from src.tools.registry import ToolRegistry, get_registry
from src.tools import task_management as tm_mod
from src.tools import scheduling as sc_mod
# ββ base tool tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_tool_result_dataclass():
r = ToolResult(success=True, data={"foo": "bar"}, duration_seconds=1.5)
assert r.success is True
assert r.data == {"foo": "bar"}
assert r.duration_seconds == 1.5
def test_base_tool_validate_input_returns_tuple():
class DummyTool(BaseTool):
name = "Dummy"
description = ""
def execute(self, input_data):
return ToolResult(success=True)
tool = DummyTool()
valid, err = tool.validate_input({})
assert valid is True
assert err is None
def test_base_tool_call_wraps_validation():
class AlwaysFail(BaseTool):
name = "AlwaysFail"
description = ""
def validate_input(self, input_data):
return False, "nope"
def execute(self, input_data):
return ToolResult(success=True)
result = AlwaysFail().call({})
assert result.success is False
assert "nope" in result.error
# ββ registry tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_registry_singleton():
r1 = get_registry()
r2 = get_registry()
assert r1 is r2
def test_registry_register_and_get():
class DummyTool(BaseTool):
name = "TestTool"
description = ""
def execute(self, input_data):
return ToolResult(success=True)
registry = ToolRegistry()
registry.register(DummyTool())
assert registry.get("TestTool") is not None
assert registry.get("NonExistent") is None
def test_registry_list():
registry = ToolRegistry()
names = registry.list()
assert isinstance(names, list)
def test_registry_call_unknown_raises():
registry = ToolRegistry()
with pytest.raises(KeyError):
registry.call("NoSuchTool", {})
# ββ cron parsing tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_parse_cron_valid():
from src.tools.scheduling import parse_cron, cron_to_human
for expr in ["* * * * *", "0 9 * * *", "*/5 * * * *", "30 14 1 * *"]:
valid, err = parse_cron(expr)
assert valid, f"Should be valid: {expr}, got {err}"
def test_parse_cron_invalid():
from src.tools.scheduling import parse_cron
for expr in ["* * *", "not a cron", "60 * * * *", "* * * * * *"]:
valid, err = parse_cron(expr)
assert not valid, f"Should be invalid: {expr}"
def test_cron_to_human():
from src.tools.scheduling import cron_to_human
assert cron_to_human("*/5 * * * *") == "every 5 minutes"
assert cron_to_human("0 * * * *") == "every hour"
def test_next_cron_run():
from datetime import datetime as dt
from src.tools.scheduling import next_cron_run
# "every minute" should fire within 2 minutes
next_min = next_cron_run("* * * * *")
assert next_min is not None
assert next_min > dt.now()
# ββ scheduling tool tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_cron_create_validate_invalid_cron():
tool = sc_mod.CronCreateTool()
valid, err = tool.validate_input({"cron": "not valid"})
assert not valid
assert "Invalid cron" in err
def test_cron_create_validate_missing_prompt():
tool = sc_mod.CronCreateTool()
valid, err = tool.validate_input({"cron": "0 9 * * *"})
assert not valid
assert "prompt" in err.lower()
def test_cron_create_success():
tool = sc_mod.CronCreateTool()
result = tool.call({"cron": "0 9 * * *", "prompt": "Good morning", "durable": True})
assert result.success
assert "id" in result.data
def test_cron_list_empty():
tool = sc_mod.CronListTool()
result = tool.call({})
assert result.success
assert result.data["total"] >= 0
def test_cron_delete_unknown():
tool = sc_mod.CronDeleteTool()
result = tool.call({"id": "does-not-exist"})
assert not result.success
# ββ task management tool tests ββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_task_create_success():
tool = tm_mod.TaskCreateTool()
result = tool.call({
"subject": "Test task",
"description": "A description",
"priority": "high",
})
assert result.success
assert "id" in result.data
assert result.data["subject"] == "Test task"
def test_task_create_missing_subject():
tool = tm_mod.TaskCreateTool()
result = tool.call({})
assert not result.success
assert "subject" in result.error.lower()
def test_task_list():
tool = tm_mod.TaskListTool()
result = tool.call({})
assert result.success
assert "tasks" in result.data
def test_task_update_not_found():
tool = tm_mod.TaskUpdateTool()
result = tool.call({"id": "no-such-id"})
assert not result.success
def test_task_delete_unknown():
tool = tm_mod.TaskDeleteTool()
result = tool.call({"id": "no-such-id"})
assert not result.success
# ββ web search tool tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_web_search_validate_empty_query():
# Import inside to avoid import errors if ddgs not installed
import importlib
import src.tools.web_search as ws
importlib.reload(ws)
tool = ws.WebSearchTool()
valid, err = tool.validate_input({"query": ""})
assert not valid
def test_web_search_validate_too_short():
import src.tools.web_search as ws
tool = ws.WebSearchTool()
valid, err = tool.validate_input({"query": "a"})
assert not valid
def test_web_search_validate_both_domains():
import src.tools.web_search as ws
tool = ws.WebSearchTool()
valid, err = tool.validate_input({
"query": "test",
"allowed_domains": ["example.com"],
"blocked_domains": ["foo.com"],
})
assert not valid
def test_web_search_no_ddgs():
import src.tools.web_search as ws
# Force DDGS to None to test the import error path
original = ws.DDGS
ws.DDGS = None
tool = ws.WebSearchTool()
result = tool.execute({"query": "test"})
ws.DDGS = original
assert not result.success
assert "not installed" in result.error
if __name__ == "__main__":
pytest.main([__file__, "-v"])
|