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.
184 lines
7.4 KiB
Python
184 lines
7.4 KiB
Python
"""
|
|
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"
|
|
)
|