latest updates

This commit is contained in:
Tanmay Karande
2026-02-15 15:02:58 -05:00
parent 438bb80416
commit 41b2f9a593
24 changed files with 3883 additions and 388 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Test package

200
tests/test_base_adapter.py Normal file
View File

@@ -0,0 +1,200 @@
"""
Tests for the Base Adapter and IncomingMessage.
"""
import pytest
from datetime import datetime, timezone
from adapters.base import BaseAdapter, IncomingMessage
# ---------------------------------------------------------------------------
# Concrete adapter for testing (implements all abstract methods)
# ---------------------------------------------------------------------------
class MockAdapter(BaseAdapter):
"""A minimal concrete adapter for testing BaseAdapter."""
def __init__(self):
super().__init__()
self.sent_messages: list[dict] = []
self._started = False
@property
def source_name(self) -> str:
return "mock"
def start(self) -> None:
self._started = True
def start_async(self) -> None:
self._started = True
def stop(self) -> None:
self._started = False
def send_message(
self,
channel_id: str,
text: str,
thread_id: str | None = None,
) -> None:
self.sent_messages.append({
"channel_id": channel_id,
"text": text,
"thread_id": thread_id,
})
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def adapter():
return MockAdapter()
@pytest.fixture
def sample_message():
return IncomingMessage(
text="Hello world",
user_id="U123",
user_name="testuser",
channel_id="C456",
channel_name="general",
conversation_id="conv-789",
source="mock",
is_dm=False,
raw_event={"thread_id": "T100"},
)
# ---------------------------------------------------------------------------
# Tests: IncomingMessage
# ---------------------------------------------------------------------------
class TestIncomingMessage:
def test_create_message(self, sample_message):
assert sample_message.text == "Hello world"
assert sample_message.user_id == "U123"
assert sample_message.user_name == "testuser"
assert sample_message.channel_id == "C456"
assert sample_message.source == "mock"
assert sample_message.is_dm is False
def test_timestamp_default(self):
msg = IncomingMessage(
text="test",
user_id="U1",
user_name="user",
channel_id="C1",
channel_name="ch",
conversation_id="conv",
source="test",
is_dm=True,
)
assert msg.timestamp is not None
assert msg.timestamp.tzinfo is not None # has timezone
def test_raw_event_default(self):
msg = IncomingMessage(
text="test",
user_id="U1",
user_name="user",
channel_id="C1",
channel_name="ch",
conversation_id="conv",
source="test",
is_dm=False,
)
assert msg.raw_event == {}
# ---------------------------------------------------------------------------
# Tests: BaseAdapter
# ---------------------------------------------------------------------------
class TestBaseAdapter:
def test_register_handler(self, adapter):
handler = lambda msg: "response"
adapter.on_message(handler)
assert len(adapter._message_handlers) == 1
def test_on_message_as_decorator(self, adapter):
@adapter.on_message
def my_handler(msg):
return "decorated response"
assert len(adapter._message_handlers) == 1
assert my_handler("test") == "decorated response"
def test_dispatch_calls_handler(self, adapter, sample_message):
responses = []
@adapter.on_message
def handler(msg):
responses.append(msg.text)
return f"reply to: {msg.text}"
adapter._dispatch(sample_message)
assert responses == ["Hello world"]
def test_dispatch_sends_response(self, adapter, sample_message):
@adapter.on_message
def handler(msg):
return "Auto reply"
adapter._dispatch(sample_message)
assert len(adapter.sent_messages) == 1
assert adapter.sent_messages[0]["text"] == "Auto reply"
assert adapter.sent_messages[0]["channel_id"] == "C456"
def test_dispatch_no_response(self, adapter, sample_message):
@adapter.on_message
def handler(msg):
return None # explicit no response
adapter._dispatch(sample_message)
assert len(adapter.sent_messages) == 0
def test_dispatch_handler_error(self, adapter, sample_message):
@adapter.on_message
def bad_handler(msg):
raise ValueError("Something broke")
# Should not raise — dispatch catches errors
adapter._dispatch(sample_message)
# Should send error message
assert len(adapter.sent_messages) == 1
assert "Something went wrong" in adapter.sent_messages[0]["text"]
def test_multiple_handlers(self, adapter, sample_message):
calls = []
@adapter.on_message
def handler1(msg):
calls.append("h1")
return None
@adapter.on_message
def handler2(msg):
calls.append("h2")
return "from h2"
adapter._dispatch(sample_message)
assert calls == ["h1", "h2"]
assert len(adapter.sent_messages) == 1 # only h2 returned a response
def test_source_name(self, adapter):
assert adapter.source_name == "mock"
def test_start_stop(self, adapter):
adapter.start()
assert adapter._started is True
adapter.stop()
assert adapter._started is False

140
tests/test_scheduler.py Normal file
View File

@@ -0,0 +1,140 @@
"""
Tests for the Scheduler Store (SQLite persistence).
"""
import os
import tempfile
from datetime import datetime, timezone
import pytest
from scheduler.store import JobStore, ScheduledJob
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def db_path(tmp_path):
"""Provide a temp database path."""
return str(tmp_path / "test_scheduler.db")
@pytest.fixture
def store(db_path):
"""Create a fresh JobStore."""
return JobStore(db_path=db_path)
def _make_job(
cron_expr: str | None = "*/5 * * * *",
prompt: str = "Test prompt",
channel_id: str = "C123",
channel_type: str = "slack",
) -> ScheduledJob:
"""Helper to create a ScheduledJob."""
return ScheduledJob(
id=JobStore.new_id(),
cron_expr=cron_expr,
prompt=prompt,
channel_id=channel_id,
channel_type=channel_type,
created_at=datetime.now(timezone.utc).isoformat(),
)
# ---------------------------------------------------------------------------
# Tests: JobStore
# ---------------------------------------------------------------------------
class TestJobStore:
def test_add_and_get(self, store):
job = _make_job(prompt="Hello scheduler")
store.add(job)
retrieved = store.get(job.id)
assert retrieved is not None
assert retrieved.id == job.id
assert retrieved.prompt == "Hello scheduler"
assert retrieved.cron_expr == "*/5 * * * *"
def test_remove(self, store):
job = _make_job()
store.add(job)
assert store.remove(job.id) is True
assert store.get(job.id) is None
def test_remove_nonexistent(self, store):
assert store.remove("nonexistent") is False
def test_list_all(self, store):
job1 = _make_job(prompt="Job 1")
job2 = _make_job(prompt="Job 2")
store.add(job1)
store.add(job2)
all_jobs = store.list_all()
assert len(all_jobs) == 2
def test_list_recurring(self, store):
cron_job = _make_job(cron_expr="0 9 * * *", prompt="Recurring")
oneshot_job = _make_job(cron_expr=None, prompt="One-shot")
store.add(cron_job)
store.add(oneshot_job)
recurring = store.list_recurring()
assert len(recurring) == 1
assert recurring[0].prompt == "Recurring"
def test_clear_oneshot(self, store):
cron_job = _make_job(cron_expr="0 9 * * *")
oneshot1 = _make_job(cron_expr=None, prompt="OS 1")
oneshot2 = _make_job(cron_expr=None, prompt="OS 2")
store.add(cron_job)
store.add(oneshot1)
store.add(oneshot2)
removed = store.clear_oneshot()
assert removed == 2
remaining = store.list_all()
assert len(remaining) == 1
assert remaining[0].is_recurring
def test_is_recurring(self):
cron = _make_job(cron_expr="*/5 * * * *")
oneshot = _make_job(cron_expr=None)
assert cron.is_recurring is True
assert oneshot.is_recurring is False
def test_persistence(self, db_path):
"""Jobs survive store re-creation."""
store1 = JobStore(db_path=db_path)
job = _make_job(prompt="Persistent job")
store1.add(job)
# Create a new store instance pointing to same DB
store2 = JobStore(db_path=db_path)
retrieved = store2.get(job.id)
assert retrieved is not None
assert retrieved.prompt == "Persistent job"
def test_new_id_unique(self):
ids = {JobStore.new_id() for _ in range(100)}
assert len(ids) == 100 # all unique
def test_job_with_metadata(self, store):
job = _make_job()
job.thread_id = "T456"
job.user_name = "Test User"
store.add(job)
retrieved = store.get(job.id)
assert retrieved is not None
assert retrieved.thread_id == "T456"
assert retrieved.user_name == "Test User"
def test_telegram_channel_type(self, store):
job = _make_job(channel_type="telegram", channel_id="987654")
store.add(job)
retrieved = store.get(job.id)
assert retrieved is not None
assert retrieved.channel_type == "telegram"
assert retrieved.channel_id == "987654"

272
tests/test_skills.py Normal file
View File

@@ -0,0 +1,272 @@
"""
Tests for the Skills System.
"""
import os
import tempfile
import pytest
from skills.skills import Skill, SkillsManager
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def skills_workspace(tmp_path):
"""Create a temp workspace with sample skills."""
skills_dir = tmp_path / "skills"
# Skill 1: weather
weather_dir = skills_dir / "weather"
weather_dir.mkdir(parents=True)
(weather_dir / "SKILL.md").write_text(
"""---
name: weather
description: Check weather for any city
triggers: [weather, forecast, temperature, rain]
---
# Weather Skill
When the user asks about weather:
1. Extract the city name
2. Provide info based on your knowledge
"""
)
# Skill 2: coding
coding_dir = skills_dir / "coding"
coding_dir.mkdir(parents=True)
(coding_dir / "SKILL.md").write_text(
"""---
name: coding
description: Help with programming tasks
triggers: [code, python, javascript, bug, debug]
---
# Coding Skill
When the user asks about coding:
- Ask what language they're working in
- Provide code snippets with explanations
"""
)
# Skill 3: invalid (no SKILL.md)
invalid_dir = skills_dir / "empty_skill"
invalid_dir.mkdir(parents=True)
# No SKILL.md here
# Skill 4: minimal (no explicit triggers)
minimal_dir = skills_dir / "minimal"
minimal_dir.mkdir(parents=True)
(minimal_dir / "SKILL.md").write_text(
"""---
name: minimal_skill
description: A minimal skill
---
This is a minimal skill with no explicit triggers.
"""
)
return tmp_path
@pytest.fixture
def empty_workspace(tmp_path):
"""Create a temp workspace with no skills directory."""
return tmp_path
# ---------------------------------------------------------------------------
# Tests: Skill Dataclass
# ---------------------------------------------------------------------------
class TestSkill:
def test_matches_trigger(self):
skill = Skill(
name="weather",
description="Weather skill",
triggers=["weather", "forecast"],
body="# Weather",
path="/fake/path",
)
assert skill.matches("What's the weather today?")
assert skill.matches("Give me the FORECAST")
assert not skill.matches("Tell me a joke")
def test_matches_is_case_insensitive(self):
skill = Skill(
name="test",
description="Test",
triggers=["Python"],
body="body",
path="/fake",
)
assert skill.matches("I'm learning python")
assert skill.matches("PYTHON is great")
assert skill.matches("Python 3.14")
# ---------------------------------------------------------------------------
# Tests: SkillsManager Loading
# ---------------------------------------------------------------------------
class TestSkillsManagerLoading:
def test_load_all_finds_skills(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
loaded = manager.load_all()
# Should find weather, coding, minimal (not empty_skill which has no SKILL.md)
assert len(loaded) == 3
names = {s.name for s in loaded}
assert "weather" in names
assert "coding" in names
assert "minimal_skill" in names
def test_load_parses_frontmatter(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
weather = next(s for s in manager.skills if s.name == "weather")
assert weather.description == "Check weather for any city"
assert "weather" in weather.triggers
assert "forecast" in weather.triggers
assert "temperature" in weather.triggers
assert "rain" in weather.triggers
def test_load_parses_body(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
weather = next(s for s in manager.skills if s.name == "weather")
assert "# Weather Skill" in weather.body
assert "Extract the city name" in weather.body
def test_empty_workspace(self, empty_workspace):
manager = SkillsManager(str(empty_workspace))
loaded = manager.load_all()
assert loaded == []
def test_minimal_skill_uses_name_as_trigger(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
minimal = next(s for s in manager.skills if s.name == "minimal_skill")
assert minimal.triggers == ["minimal_skill"]
def test_reload_rediscovers(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
assert len(manager.skills) == 3
# Add a new skill
new_dir = skills_workspace / "skills" / "newskill"
new_dir.mkdir()
(new_dir / "SKILL.md").write_text(
"---\nname: newskill\ndescription: New\ntriggers: [new]\n---\nNew body"
)
reloaded = manager.reload()
assert len(reloaded) == 4
# ---------------------------------------------------------------------------
# Tests: SkillsManager Matching
# ---------------------------------------------------------------------------
class TestSkillsManagerMatching:
def test_get_relevant_finds_matching(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
relevant = manager.get_relevant("What's the weather?")
names = {s.name for s in relevant}
assert "weather" in names
assert "coding" not in names
def test_get_relevant_multiple_matches(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
# This should match both weather (temperature) and coding (debug)
relevant = manager.get_relevant("debug the temperature sensor code")
names = {s.name for s in relevant}
assert "weather" in names
assert "coding" in names
def test_get_relevant_no_matches(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
relevant = manager.get_relevant("Tell me a joke about cats")
assert relevant == []
# ---------------------------------------------------------------------------
# Tests: Context Generation
# ---------------------------------------------------------------------------
class TestSkillsManagerContext:
def test_get_context_with_matching(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
context = manager.get_context("forecast for tomorrow")
assert "# Active Skills" in context
assert "Weather Skill" in context
def test_get_context_empty_when_no_match(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
context = manager.get_context("random unrelated message")
assert context == ""
def test_get_all_context(self, skills_workspace):
manager = SkillsManager(str(skills_workspace))
manager.load_all()
summary = manager.get_all_context()
assert "# Available Skills" in summary
assert "weather" in summary
assert "coding" in summary
def test_get_all_context_empty_workspace(self, empty_workspace):
manager = SkillsManager(str(empty_workspace))
manager.load_all()
assert manager.get_all_context() == ""
# ---------------------------------------------------------------------------
# Tests: Frontmatter Parsing Edge Cases
# ---------------------------------------------------------------------------
class TestFrontmatterParsing:
def test_parse_list_inline_brackets(self):
result = SkillsManager._parse_list("[a, b, c]")
assert result == ["a", "b", "c"]
def test_parse_list_no_brackets(self):
result = SkillsManager._parse_list("a, b, c")
assert result == ["a", "b", "c"]
def test_parse_list_quoted_items(self):
result = SkillsManager._parse_list("['hello', \"world\"]")
assert result == ["hello", "world"]
def test_parse_list_empty(self):
result = SkillsManager._parse_list("")
assert result == []
def test_split_frontmatter(self):
content = "---\nname: test\n---\nBody here"
fm, body = SkillsManager._split_frontmatter(content)
assert "name: test" in fm
assert body == "Body here"
def test_split_no_frontmatter(self):
content = "Just a body with no frontmatter"
fm, body = SkillsManager._split_frontmatter(content)
assert fm == ""
assert body == content