From 1add224599c96ae6acd8d3ccd9cc5b0a80912c32 Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:35:58 -0700 Subject: [PATCH] test: add 59 tests for system prompt, Slack messaging, and connector health - test_deep_research_prompt.py (8 tests): date injection, time, day of week, dynamic generation, response types, tool descriptions, /no_think, Jarvis name - test_slack_messaging.py (7 tests): connect with/without tokens, error state, disconnect, handler registration, send without token - test_connector_health.py (44 tests): all 11 connectors instantiate, have required methods, report not-connected without creds, have metadata. Plus KnowledgeStore data presence checks. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/agents/test_deep_research_prompt.py | 89 ++++++++++++ tests/channels/test_slack_messaging.py | 91 +++++++++++++ tests/connectors/test_connector_health.py | 158 ++++++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 tests/agents/test_deep_research_prompt.py create mode 100644 tests/channels/test_slack_messaging.py create mode 100644 tests/connectors/test_connector_health.py diff --git a/tests/agents/test_deep_research_prompt.py b/tests/agents/test_deep_research_prompt.py new file mode 100644 index 00000000..93a01466 --- /dev/null +++ b/tests/agents/test_deep_research_prompt.py @@ -0,0 +1,89 @@ +"""Tests for DeepResearch system prompt — date injection and adaptive behavior.""" + +from __future__ import annotations + +import re +from datetime import datetime +from unittest.mock import MagicMock + +import pytest + + +def test_system_prompt_contains_current_date() -> None: + """The system prompt includes today's date.""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + today = datetime.now().strftime("%B %d, %Y") + assert today in prompt, f"Expected '{today}' in prompt" + + +def test_system_prompt_contains_current_time() -> None: + """The system prompt includes the current time (hour).""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + # Just check that a time-like pattern exists (e.g. "02:28 PM") + assert re.search(r"\d{1,2}:\d{2} [AP]M", prompt), "No time found in prompt" + + +def test_system_prompt_contains_day_of_week() -> None: + """The system prompt includes the day of week.""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + day = datetime.now().strftime("%A") + assert day in prompt, f"Expected '{day}' in prompt" + + +def test_system_prompt_is_dynamic() -> None: + """Each call to _build_system_prompt returns a fresh prompt with current time.""" + from openjarvis.agents.deep_research import _build_system_prompt + + p1 = _build_system_prompt() + p2 = _build_system_prompt() + # Both should contain "Today is" — they may differ by seconds + assert "Today is" in p1 + assert "Today is" in p2 + + +def test_system_prompt_has_response_types() -> None: + """The prompt describes multiple response types (not just deep research).""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + assert "Casual" in prompt or "conversational" in prompt + assert "Quick data lookup" in prompt or "Quick lookup" in prompt + assert "Deep research" in prompt + assert "People lookup" in prompt + assert "digest" in prompt.lower() + assert "Meeting prep" in prompt or "meeting" in prompt.lower() + assert "Task" in prompt or "action item" in prompt.lower() + assert "Contact analysis" in prompt or "haven't I messaged" in prompt + + +def test_system_prompt_has_tools() -> None: + """The prompt describes all 4 tools.""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + assert "knowledge_search" in prompt + assert "knowledge_sql" in prompt + assert "scan_chunks" in prompt + assert "think" in prompt + + +def test_system_prompt_has_no_think_directive() -> None: + """The prompt starts with /no_think for Qwen compatibility.""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + assert prompt.startswith("/no_think") + + +def test_system_prompt_mentions_jarvis() -> None: + """The agent identifies as Jarvis.""" + from openjarvis.agents.deep_research import _build_system_prompt + + prompt = _build_system_prompt() + assert "Jarvis" in prompt diff --git a/tests/channels/test_slack_messaging.py b/tests/channels/test_slack_messaging.py new file mode 100644 index 00000000..7fe3a51e --- /dev/null +++ b/tests/channels/test_slack_messaging.py @@ -0,0 +1,91 @@ +"""Tests for Slack messaging channel — Socket Mode binding and message handling.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + + +def test_slack_channel_connect_with_tokens() -> None: + """SlackChannel.connect() succeeds with bot_token + app_token.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel( + bot_token="xoxb-test-token", + app_token="xapp-test-token", + ) + # Mock the SocketModeClient import to avoid real connection + with patch("openjarvis.channels.slack.SlackChannel._socket_mode_loop"): + ch.connect() + + from openjarvis.channels._stubs import ChannelStatus + + assert ch.status() == ChannelStatus.CONNECTED + + +def test_slack_channel_send_only_without_app_token() -> None: + """SlackChannel with only bot_token enters send-only mode.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel(bot_token="xoxb-test-token") + ch.connect() + + from openjarvis.channels._stubs import ChannelStatus + + assert ch.status() == ChannelStatus.CONNECTED + assert ch._listener_thread is None # No Socket Mode listener + + +def test_slack_channel_no_token_errors() -> None: + """SlackChannel without any token enters error state.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel() + ch.connect() + + from openjarvis.channels._stubs import ChannelStatus + + assert ch.status() == ChannelStatus.ERROR + + +def test_slack_channel_disconnect() -> None: + """SlackChannel.disconnect() sets status to disconnected.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel(bot_token="xoxb-test-token") + ch.connect() + ch.disconnect() + + from openjarvis.channels._stubs import ChannelStatus + + assert ch.status() == ChannelStatus.DISCONNECTED + + +def test_slack_channel_handler_registration() -> None: + """on_message registers a handler that receives messages.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel(bot_token="xoxb-test-token") + handler = MagicMock() + ch.on_message(handler) + assert handler in ch._handlers + + +def test_slack_channel_send_without_connection_fails_gracefully() -> None: + """send() to an unconnected channel returns False without crashing.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel() # No token + result = ch.send("C123456", "Hello from test") + assert result is False + + +def test_slack_channel_send_with_token_is_callable() -> None: + """send() with a token is callable and returns a bool.""" + from openjarvis.channels.slack import SlackChannel + + ch = SlackChannel(bot_token="xoxb-test-token") + # Don't actually call the API, just verify interface + assert callable(ch.send) + assert hasattr(ch, "send") diff --git a/tests/connectors/test_connector_health.py b/tests/connectors/test_connector_health.py new file mode 100644 index 00000000..03b8fd1d --- /dev/null +++ b/tests/connectors/test_connector_health.py @@ -0,0 +1,158 @@ +"""Health check tests for all data source connectors. + +Verifies that each connector can be instantiated, reports connection +status correctly, and has the required interface methods. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + + +# All connectors that should be testable without credentials +_LOCAL_CONNECTORS = [ + ("apple_notes", "openjarvis.connectors.apple_notes", "AppleNotesConnector"), + ("imessage", "openjarvis.connectors.imessage", "IMessageConnector"), +] + +_TOKEN_CONNECTORS = [ + ("gmail_imap", "openjarvis.connectors.gmail_imap", "GmailIMAPConnector"), + ("outlook", "openjarvis.connectors.outlook", "OutlookConnector"), + ("slack", "openjarvis.connectors.slack_connector", "SlackConnector"), + ("notion", "openjarvis.connectors.notion", "NotionConnector"), + ("granola", "openjarvis.connectors.granola", "GranolaConnector"), + ("gdrive", "openjarvis.connectors.gdrive", "GDriveConnector"), + ("gcalendar", "openjarvis.connectors.gcalendar", "GCalendarConnector"), + ("gcontacts", "openjarvis.connectors.gcontacts", "GContactsConnector"), + ("dropbox", "openjarvis.connectors.dropbox", "DropboxConnector"), +] + +_ALL_CONNECTORS = _LOCAL_CONNECTORS + _TOKEN_CONNECTORS + + +@pytest.mark.parametrize( + "connector_id,module_path,class_name", + _ALL_CONNECTORS, + ids=[c[0] for c in _ALL_CONNECTORS], +) +def test_connector_instantiates( + connector_id: str, module_path: str, class_name: str, +) -> None: + """Every connector can be instantiated without errors.""" + import importlib + + mod = importlib.import_module(module_path) + cls = getattr(mod, class_name) + instance = cls() + assert instance is not None + + +@pytest.mark.parametrize( + "connector_id,module_path,class_name", + _ALL_CONNECTORS, + ids=[c[0] for c in _ALL_CONNECTORS], +) +def test_connector_has_required_methods( + connector_id: str, module_path: str, class_name: str, +) -> None: + """Every connector implements the BaseConnector interface.""" + import importlib + + mod = importlib.import_module(module_path) + cls = getattr(mod, class_name) + instance = cls() + assert hasattr(instance, "is_connected") + assert hasattr(instance, "disconnect") + assert hasattr(instance, "sync") + assert hasattr(instance, "sync_status") + assert callable(instance.is_connected) + assert callable(instance.disconnect) + assert callable(instance.sync) + + +@pytest.mark.parametrize( + "connector_id,module_path,class_name", + _TOKEN_CONNECTORS, + ids=[c[0] for c in _TOKEN_CONNECTORS], +) +def test_connector_not_connected_without_creds( + connector_id: str, module_path: str, class_name: str, +) -> None: + """Token connectors report not connected without credentials.""" + import importlib + + mod = importlib.import_module(module_path) + cls = getattr(mod, class_name) + # Use a temp credentials path that doesn't exist + try: + instance = cls(credentials_path="/tmp/nonexistent_creds.json") + except TypeError: + instance = cls() + assert instance.is_connected() is False + + +@pytest.mark.parametrize( + "connector_id,module_path,class_name", + _ALL_CONNECTORS, + ids=[c[0] for c in _ALL_CONNECTORS], +) +def test_connector_has_metadata( + connector_id: str, module_path: str, class_name: str, +) -> None: + """Every connector has connector_id, display_name, and auth_type.""" + import importlib + + mod = importlib.import_module(module_path) + cls = getattr(mod, class_name) + assert hasattr(cls, "connector_id") + assert hasattr(cls, "display_name") + assert hasattr(cls, "auth_type") + assert cls.connector_id == connector_id + assert isinstance(cls.display_name, str) + assert cls.auth_type in ("oauth", "local", "bridge", "filesystem") + + +def test_knowledge_store_has_data() -> None: + """KnowledgeStore at default path has data (if it exists).""" + from openjarvis.connectors.store import KnowledgeStore + from openjarvis.core.config import DEFAULT_CONFIG_DIR + + db_path = DEFAULT_CONFIG_DIR / "knowledge.db" + if not db_path.exists(): + pytest.skip("No knowledge.db found") + + store = KnowledgeStore(str(db_path)) + count = store.count() + assert count > 0, "KnowledgeStore has no data" + + # Check sources exist + rows = store._conn.execute( + "SELECT DISTINCT source FROM knowledge_chunks" + ).fetchall() + sources = [r[0] for r in rows] + assert len(sources) > 0, "No sources in KnowledgeStore" + store.close() + + +def test_knowledge_store_sources_have_chunks() -> None: + """Each connected source has at least 1 chunk in the store.""" + from openjarvis.connectors.store import KnowledgeStore + from openjarvis.core.config import DEFAULT_CONFIG_DIR + + db_path = DEFAULT_CONFIG_DIR / "knowledge.db" + if not db_path.exists(): + pytest.skip("No knowledge.db found") + + store = KnowledgeStore(str(db_path)) + rows = store._conn.execute( + "SELECT source, COUNT(*) as n FROM knowledge_chunks " + "GROUP BY source ORDER BY n DESC" + ).fetchall() + + for source, count in rows: + assert count > 0, f"Source '{source}' has 0 chunks" + + store.close()