Add Discord-native hybrid front-end for Jarvis (bot + bridge)
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.
This commit is contained in:
javis-bot
2026-06-09 14:51:05 +09:00
parent a5bf8d1826
commit c4abf63f38
308 changed files with 94135 additions and 1 deletions

124
tests/conftest.py Normal file
View File

@@ -0,0 +1,124 @@
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