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
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:
183
tests/test_config_models.py
Normal file
183
tests/test_config_models.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user