Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Transform isair/jarvis into a Discord-controlled voice assistant running on the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact. - bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral), voice channel join + voice receive/playback, pluggable VNC screen broadcast (selfbot live / noVNC / screenshot) - bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS behind a thin localhost HTTP API - .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite, docs/language-comparison.md and docs/vnc-xfce-setup.md Language decision: hybrid (Python brain + Node/bun Discord layer) because Discord blocks bot video; native screen broadcast only works via a Node selfbot library.
125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import pytest
|
|
|
|
# Robustly locate repository root (directory containing src/jarvis)
|
|
_this_file = Path(__file__).resolve()
|
|
ROOT = None
|
|
for parent in _this_file.parents:
|
|
if (parent / "src" / "jarvis").exists():
|
|
ROOT = parent
|
|
break
|
|
if ROOT is None:
|
|
# Fallback to two levels up
|
|
ROOT = _this_file.parent.parent
|
|
|
|
SRC = ROOT / "src"
|
|
# Both ROOT and SRC are on sys.path so tests can write either
|
|
# ``from src.jarvis.x import ...`` (older style, ``src.`` prefix)
|
|
# or
|
|
# ``from jarvis.x import ...`` (newer style, no prefix)
|
|
# CAUTION: those two import paths resolve to *distinct module instances*.
|
|
# A monkeypatch on ``src.jarvis.memory.conversation.X`` does NOT take
|
|
# effect on ``jarvis.memory.conversation.X`` and vice versa. When a test
|
|
# stubs out a symbol the production code calls, you MUST patch the same
|
|
# module instance the production code resolves at runtime. Production code
|
|
# in ``src/`` imports without the ``src.`` prefix (e.g. inside endpoint
|
|
# handlers it's ``from jarvis.memory.conversation import ...``), so a test
|
|
# that monkeypatches a symbol used by production should also import
|
|
# without the prefix. This is the convention going forward; the older
|
|
# ``from src.X`` style is left in place to avoid a churn-only sweep, but
|
|
# do not adopt it for new tests that monkeypatch.
|
|
# Add repository root so that 'src' is a package prefix.
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
# Also add the src directory (optional, for backwards compatibility with direct 'jarvis' imports)
|
|
if str(SRC) not in sys.path:
|
|
sys.path.insert(0, str(SRC))
|
|
|
|
|
|
@dataclass
|
|
class MockConfig:
|
|
"""Minimal config object for unit tests that need a config."""
|
|
ollama_base_url: str = "http://localhost:11434"
|
|
ollama_chat_model: str = "gemma4:e2b"
|
|
ollama_embed_model: str = "nomic-embed-text"
|
|
db_path: str = ":memory:"
|
|
sqlite_vss_path: Optional[str] = None
|
|
voice_debug: bool = True
|
|
tts_enabled: bool = False
|
|
tts_engine: str = "piper"
|
|
tts_voice: Optional[str] = None
|
|
tts_rate: int = 200
|
|
tts_piper_model_path: Optional[str] = None
|
|
tts_piper_speaker: Optional[int] = None
|
|
tts_piper_length_scale: float = 1.0
|
|
tts_piper_noise_scale: float = 0.667
|
|
tts_piper_noise_w: float = 0.8
|
|
tts_piper_sentence_silence: float = 0.2
|
|
tts_chatterbox_device: str = "cpu"
|
|
tts_chatterbox_audio_prompt: Optional[str] = None
|
|
tts_chatterbox_exaggeration: float = 0.5
|
|
tts_chatterbox_cfg_weight: float = 0.5
|
|
web_search_enabled: bool = True
|
|
brave_search_api_key: str = ""
|
|
wikipedia_fallback_enabled: bool = True
|
|
llm_tools_timeout_sec: float = 8.0
|
|
llm_embed_timeout_sec: float = 10.0
|
|
llm_chat_timeout_sec: float = 45.0
|
|
agentic_max_turns: int = 8
|
|
tool_selection_strategy: str = "embedding"
|
|
tool_router_model: str = ""
|
|
memory_enrichment_max_results: int = 5
|
|
memory_enrichment_source: str = "diary"
|
|
location_enabled: bool = True
|
|
location_ip_address: Optional[str] = None
|
|
location_auto_detect: bool = False
|
|
location_cgnat_resolve_public_ip: bool = False
|
|
dialogue_memory_timeout: int = 300
|
|
llm_thinking_enabled: bool = False
|
|
intent_judge_thinking_enabled: bool = False
|
|
dictation_thinking_enabled: bool = False
|
|
mcps: Dict[str, Any] = field(default_factory=dict)
|
|
use_stdin: bool = True
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config():
|
|
"""Provide a mock configuration for unit tests."""
|
|
return MockConfig()
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
"""Provide an in-memory database for unit tests."""
|
|
from jarvis.memory.db import Database
|
|
database = Database(":memory:", sqlite_vss_path=None)
|
|
yield database
|
|
database.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def dialogue_memory():
|
|
"""Provide a dialogue memory instance for unit tests."""
|
|
from jarvis.memory.conversation import DialogueMemory
|
|
return DialogueMemory(inactivity_timeout=300, max_interactions=20)
|
|
|
|
|
|
@pytest.fixture
|
|
def qapp():
|
|
"""Provide a shared QApplication for Qt-based UI tests.
|
|
|
|
Qt requires exactly one QApplication per process. Re-uses an existing
|
|
instance when present so repeated test runs inside a single session
|
|
don't error.
|
|
"""
|
|
from PyQt6.QtWidgets import QApplication
|
|
app = QApplication.instance()
|
|
if app is None:
|
|
app = QApplication([])
|
|
yield app
|
|
|