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

183
tests/test_config_models.py Normal file
View File

@@ -0,0 +1,183 @@
"""
Tests for model configuration in config.py.
Tests the centralized model definitions that serve as the single source of truth
for supported chat models across the application.
"""
import pytest
from jarvis.config import (
SUPPORTED_CHAT_MODELS,
DEFAULT_CHAT_MODEL,
get_supported_model_ids,
get_default_config,
)
class TestSupportedChatModels:
"""Tests for SUPPORTED_CHAT_MODELS constant."""
def test_supported_models_is_dict(self):
"""SUPPORTED_CHAT_MODELS should be a dict."""
assert isinstance(SUPPORTED_CHAT_MODELS, dict)
def test_supported_models_not_empty(self):
"""SUPPORTED_CHAT_MODELS should have at least one model."""
assert len(SUPPORTED_CHAT_MODELS) > 0
def test_supported_models_have_required_fields(self):
"""Each model should have name, description, size, and ram fields."""
required_fields = {"name", "description", "size", "vram"}
for model_id, info in SUPPORTED_CHAT_MODELS.items():
assert isinstance(info, dict), f"{model_id} info should be a dict"
for field in required_fields:
assert field in info, f"{model_id} missing required field: {field}"
assert isinstance(info[field], str), f"{model_id}.{field} should be a string"
def test_model_ids_are_valid_format(self):
"""Model IDs should be in valid Ollama format (name:tag or just name)."""
for model_id in SUPPORTED_CHAT_MODELS:
assert isinstance(model_id, str)
assert len(model_id) > 0
# Should not have spaces
assert " " not in model_id
class TestDefaultChatModel:
"""Tests for DEFAULT_CHAT_MODEL constant."""
def test_default_model_is_string(self):
"""DEFAULT_CHAT_MODEL should be a string."""
assert isinstance(DEFAULT_CHAT_MODEL, str)
def test_default_model_in_supported_models(self):
"""DEFAULT_CHAT_MODEL must be in SUPPORTED_CHAT_MODELS."""
assert DEFAULT_CHAT_MODEL in SUPPORTED_CHAT_MODELS
def test_default_model_not_empty(self):
"""DEFAULT_CHAT_MODEL should not be empty."""
assert len(DEFAULT_CHAT_MODEL) > 0
class TestGetSupportedModelIds:
"""Tests for get_supported_model_ids() function."""
def test_returns_set(self):
"""get_supported_model_ids() should return a set."""
result = get_supported_model_ids()
assert isinstance(result, set)
def test_returns_model_ids(self):
"""get_supported_model_ids() should return the model IDs from SUPPORTED_CHAT_MODELS."""
result = get_supported_model_ids()
expected = set(SUPPORTED_CHAT_MODELS.keys())
assert result == expected
def test_contains_default_model(self):
"""get_supported_model_ids() should include DEFAULT_CHAT_MODEL."""
result = get_supported_model_ids()
assert DEFAULT_CHAT_MODEL in result
class TestDefaultConfigUsesModelConstant:
"""Tests to ensure default config uses the model constants."""
def test_default_config_uses_default_chat_model(self):
"""get_default_config() should use DEFAULT_CHAT_MODEL for ollama_chat_model."""
config = get_default_config()
assert config["ollama_chat_model"] == DEFAULT_CHAT_MODEL
def test_default_config_model_is_supported(self):
"""The default model in config should be a supported model."""
config = get_default_config()
model = config["ollama_chat_model"]
assert model in SUPPORTED_CHAT_MODELS
class TestWhisperHallucinationFilterDefaults:
"""Pin defaults for the Whisper hallucination-filter thresholds.
Both the faster-whisper `_filter_noisy_segments` path and the MLX
`_finalize_utterance` path read these via `getattr(cfg, ..., fallback)`;
the defaults must stay in sync with the `Settings` dataclass field and
the values documented in README and `listening.spec.md`.
"""
def test_no_speech_threshold_default(self):
config = get_default_config()
assert "whisper_no_speech_threshold" in config
assert config["whisper_no_speech_threshold"] == 0.5
assert 0.0 <= config["whisper_no_speech_threshold"] <= 1.0
def test_min_confidence_default(self):
config = get_default_config()
assert "whisper_min_confidence" in config
assert config["whisper_min_confidence"] == 0.3
assert 0.0 <= config["whisper_min_confidence"] <= 1.0
def test_settings_dataclass_round_trips_no_speech_threshold(self, tmp_path, monkeypatch):
"""A config file with an overridden threshold must parse through
`load_settings` into the `Settings.whisper_no_speech_threshold` field.
"""
import json as _json
from jarvis.config import load_settings
cfg_path = tmp_path / "config.json"
cfg_path.write_text(_json.dumps({"whisper_no_speech_threshold": 0.72}))
monkeypatch.setenv("JARVIS_CONFIG_PATH", str(cfg_path))
settings = load_settings()
assert settings.whisper_no_speech_threshold == pytest.approx(0.72)
class TestModelConsistency:
"""Tests for overall model configuration consistency."""
def test_all_models_have_consistent_info_structure(self):
"""All models should have the same info structure."""
if len(SUPPORTED_CHAT_MODELS) < 2:
pytest.skip("Need at least 2 models to test consistency")
first_model = next(iter(SUPPORTED_CHAT_MODELS.values()))
first_keys = set(first_model.keys())
for model_id, info in SUPPORTED_CHAT_MODELS.items():
assert set(info.keys()) == first_keys, f"{model_id} has different fields"
def test_model_names_are_descriptive(self):
"""Model names should be descriptive (not just the ID)."""
for model_id, info in SUPPORTED_CHAT_MODELS.items():
name = info["name"]
# Name should be longer than the ID (more descriptive)
assert len(name) > len(model_id), f"{model_id} name should be descriptive"
def test_vram_requirements_are_specified(self):
"""VRAM requirements should follow expected format (e.g., '8GB+')."""
for model_id, info in SUPPORTED_CHAT_MODELS.items():
vram = info["vram"]
assert "GB" in vram, f"{model_id} VRAM should specify GB"
def test_non_default_models_require_more_vram_than_default(self):
"""Non-default models need more VRAM because the intent judge (gemma4:e2b) runs alongside them.
The default model (gemma4:e2b) shares the intent judge, so its VRAM is the baseline.
Other models must load both themselves AND the intent judge, so their VRAM must be higher.
"""
import re
def _extract_vram_gb(vram_str: str) -> int:
match = re.search(r"(\d+)", vram_str)
assert match, f"Could not parse VRAM value from: {vram_str}"
return int(match.group(1))
default_vram = _extract_vram_gb(SUPPORTED_CHAT_MODELS[DEFAULT_CHAT_MODEL]["vram"])
for model_id, info in SUPPORTED_CHAT_MODELS.items():
if model_id == DEFAULT_CHAT_MODEL:
continue
model_vram = _extract_vram_gb(info["vram"])
assert model_vram > default_vram, (
f"{model_id} VRAM ({info['vram']}) should be higher than default model VRAM "
f"({SUPPORTED_CHAT_MODELS[DEFAULT_CHAT_MODEL]['vram']}) because the intent judge "
f"(gemma4:e2b) always runs alongside the chat model"
)