"""Tests for US-016: mining-style node startup CLI + live dashboard.""" from __future__ import annotations import json import socket import sys import types from pathlib import Path from unittest.mock import MagicMock, patch # --------------------------------------------------------------------------- # model_catalog tests # --------------------------------------------------------------------------- def test_curated_models_list_is_non_empty(): from meshnet_node.model_catalog import CURATED_MODELS assert len(CURATED_MODELS) >= 5 def test_model_preset_vram_for_quant(): from meshnet_node.model_catalog import CURATED_MODELS m = next(m for m in CURATED_MODELS if "Llama-3-70B" in m.name) assert m.vram_for_quant("nf4") == m.vram_nf4 assert m.vram_for_quant("int8") == m.vram_int8 assert m.vram_for_quant("bf16") == m.vram_bf16 assert m.vram_for_quant("bfloat16") == m.vram_bf16 # alias def test_model_preset_fits_vram(): from meshnet_node.model_catalog import CURATED_MODELS small = next(m for m in CURATED_MODELS if m.vram_nf4 < 10) assert small.fits_vram(small.vram_nf4, "nf4") assert not small.fits_vram(small.vram_nf4 - 1, "nf4") def test_recommended_quant_respects_vram(): from meshnet_node.model_catalog import CURATED_MODELS m = next(m for m in CURATED_MODELS if "Llama-3-70B" in m.name) # nf4=18, int8=40, bf16=140 assert m.recommended_quant(200) == "bf16" assert m.recommended_quant(50) == "int8" assert m.recommended_quant(20) == "nf4" assert m.recommended_quant(5) is None def test_models_with_insufficient_vram_are_marked(monkeypatch): from meshnet_node import wizard as wiz # Simulate 6 GB GPU gpus = [{"index": 0, "name": "RTX 3060", "vram_gb": 6.0, "backend": "cuda"}] monkeypatch.setattr(wiz, "_detect_gpus", lambda: gpus) # Phi-3 at NF4 needs 4 GB — should fit; Llama-3-70B at NF4 needs 18 GB — should not from meshnet_node.model_catalog import CURATED_MODELS phi = next(m for m in CURATED_MODELS if "Phi-3" in m.name) llama = next(m for m in CURATED_MODELS if "Llama-3-70B" in m.name) assert phi.fits_vram(6.0, "nf4") assert not llama.fits_vram(6.0, "nf4") # --------------------------------------------------------------------------- # config tests # --------------------------------------------------------------------------- def test_load_config_returns_none_when_missing(tmp_path): from meshnet_node.config import load_config assert load_config(tmp_path / "nonexistent.json") is None def test_save_and_load_config_roundtrip(tmp_path): from meshnet_node.config import save_config, load_config cfg = {"model_hf_repo": "test/model", "quantization": "nf4", "tracker_url": "http://localhost:8080"} cfg_path = tmp_path / "config.json" save_config(cfg, cfg_path) loaded = load_config(cfg_path) assert loaded == cfg def test_save_config_creates_parent_dirs(tmp_path): from meshnet_node.config import save_config, load_config nested = tmp_path / "deep" / "nested" / "config.json" save_config({"x": 1}, nested) assert nested.exists() assert load_config(nested) == {"x": 1} def test_merge_cli_overrides_applies_non_none_values(): from meshnet_node.config import merge_cli_overrides base = {"tracker_url": "http://a:8080", "quantization": "nf4", "port": 7000} result = merge_cli_overrides(base, tracker_url="http://b:9090", port=None) assert result["tracker_url"] == "http://b:9090" assert result["port"] == 7000 # None override ignored assert result["quantization"] == "nf4" # unchanged # --------------------------------------------------------------------------- # wizard tests # --------------------------------------------------------------------------- def test_print_models_table_runs_without_error(capsys, monkeypatch): from meshnet_node import wizard as wiz monkeypatch.setattr(wiz, "_detect_gpus", lambda: [{"index": 0, "name": "GPU", "vram_gb": 24.0, "backend": "cuda"}]) wiz.print_models_table() out = capsys.readouterr().out assert "Llama" in out or "Qwen" in out or "Phi" in out def test_wizard_writes_config_on_happy_path(tmp_path, monkeypatch): from meshnet_node import wizard as wiz from meshnet_node.config import load_config, save_config # Fake GPU gpus = [{"index": 0, "name": "RTX 4090", "vram_gb": 24.0, "backend": "cuda"}] monkeypatch.setattr(wiz, "_detect_gpus", lambda: gpus) # Tracker not reachable (stub) monkeypatch.setattr(wiz, "_ping_tracker", lambda url: False) # Simulate user selecting model 1 (Qwen2.5-0.5B), quant 1 (nf4), default dir, default tracker, default wallet inputs = iter([ "1", # pick Qwen2.5-0.5B-Instruct (index 1 in CURATED_MODELS) "1", # quant NF4 str(tmp_path / "models"), # download dir "http://localhost:8080", # tracker str(tmp_path / "wallet.json"), # wallet ]) monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) cfg = wiz.run_wizard(config_path_override=tmp_path / "config.json") assert cfg["model_hf_repo"] == "Qwen/Qwen2.5-0.5B-Instruct" assert cfg["quantization"] == "nf4" assert "download_dir" in cfg assert cfg["tracker_url"] == "http://localhost:8080" def test_wizard_raises_keyboard_interrupt_on_ctrl_c(monkeypatch): from meshnet_node import wizard as wiz gpus = [{"index": 0, "name": "RTX 4090", "vram_gb": 24.0, "backend": "cuda"}] monkeypatch.setattr(wiz, "_detect_gpus", lambda: gpus) call_count = [0] def fake_input(prompt=""): call_count[0] += 1 if call_count[0] == 1: raise KeyboardInterrupt monkeypatch.setattr("builtins.input", fake_input) import pytest with pytest.raises(KeyboardInterrupt): wiz.run_wizard() # --------------------------------------------------------------------------- # dashboard tests # --------------------------------------------------------------------------- def test_is_interactive_tty_false_when_not_tty(monkeypatch): from meshnet_node import dashboard as dash monkeypatch.setattr(sys.stdout, "isatty", lambda: False) assert not dash.is_interactive_tty() def test_dashboard_plain_fallback_on_keyboard_interrupt(monkeypatch): """Plain loop exits cleanly when Ctrl-C is raised.""" from meshnet_node import dashboard as dash node = MagicMock() node.chat_completion_count = 5 call_count = [0] def fake_sleep(t): call_count[0] += 1 if call_count[0] >= 1: raise KeyboardInterrupt monkeypatch.setattr(dash.time, "sleep", fake_sleep) monkeypatch.setattr(dash, "_gpu_stats", lambda: []) monkeypatch.setattr(sys.stdout, "isatty", lambda: False) cfg = {"model_name": "test-model", "quantization": "nf4"} # Should not raise dash.run_dashboard(node, cfg, start_time=dash.time.monotonic()) def test_ema_updates_correctly(): from meshnet_node.dashboard import _EMA ema = _EMA(alpha=1.0) # alpha=1.0 → always takes latest sample ema.update(10.0) assert ema.value == 10.0 ema.update(20.0) assert ema.value == 20.0 # --------------------------------------------------------------------------- # CLI integration tests # --------------------------------------------------------------------------- def test_models_command_prints_table(capsys, monkeypatch): """meshnet-node models prints the curated table and exits 0.""" from meshnet_node import wizard as wiz monkeypatch.setattr(wiz, "_detect_gpus", lambda: []) from meshnet_node.cli import main monkeypatch.setattr(sys, "argv", ["meshnet-node", "models"]) try: main() except SystemExit as exc: assert exc.code == 0 out = capsys.readouterr().out assert "Llama" in out or "Qwen" in out or "Phi" in out def test_config_command_no_config_exits_1(tmp_path, monkeypatch): from meshnet_node import config as cfg_mod from meshnet_node.cli import main monkeypatch.setattr(cfg_mod, "_DEFAULT_CONFIG_FILE", tmp_path / "nonexistent.json") monkeypatch.setattr(sys, "argv", ["meshnet-node", "config"]) with patch("meshnet_node.config.config_path", return_value=tmp_path / "nonexistent.json"): try: main() except SystemExit as exc: assert exc.code == 1 def test_config_command_prints_saved_config(tmp_path, monkeypatch, capsys): from meshnet_node import config as cfg_mod from meshnet_node.config import save_config from meshnet_node.cli import main saved = {"model_hf_repo": "meta-llama/Meta-Llama-3-70B-Instruct", "quantization": "nf4"} cfg_file = tmp_path / "config.json" save_config(saved, cfg_file) monkeypatch.setattr(sys, "argv", ["meshnet-node", "config"]) with patch("meshnet_node.config.config_path", return_value=cfg_file): with patch("meshnet_node.config.load_config", return_value=saved): try: main() except SystemExit as exc: assert exc.code == 0 out = capsys.readouterr().out data = json.loads(out.split("\n", 1)[1]) # skip the "Config: ..." header line assert data["model_hf_repo"] == saved["model_hf_repo"] def test_detect_num_layers_returns_catalog_value_without_network(monkeypatch): """detect_num_layers uses the curated catalog first — no network call.""" from meshnet_node.model_catalog import detect_num_layers # Qwen2.5-0.5B is in the catalog with 24 layers layers = detect_num_layers("Qwen/Qwen2.5-0.5B-Instruct") assert layers == 24 def test_detect_num_layers_returns_none_on_error(monkeypatch): from meshnet_node.model_catalog import detect_num_layers # Monkeypatch AutoConfig to raise import meshnet_node.model_catalog as cat monkeypatch.setattr(cat, "detect_num_layers", lambda repo: None if "bad" in repo else detect_num_layers(repo)) assert cat.detect_num_layers("bad/repo") is None def test_startup_auto_detects_shard_range(monkeypatch, tmp_path): """When shard_start/end are None, startup reads layer count from catalog.""" from meshnet_node import startup as su from meshnet_node.model_catalog import detect_num_layers calls = [] def fake_detect(repo): calls.append(repo) return 24 # Qwen2.5-0.5B monkeypatch.setattr(su, "_detect_num_layers", fake_detect) # Fake hardware detection monkeypatch.setattr(su, "detect_hardware", lambda: {"device": "cpu", "gpu_name": None, "vram_mb": 0}) # Fake wallet monkeypatch.setattr(su, "load_or_create_wallet", lambda **kw: (None, None, "fake-wallet")) # Fake TorchNodeServer class FakeNode: chat_completion_count = 0 def start(self): return 9999 def stop(self): pass import meshnet_node.startup as su2 monkeypatch.setattr(su2, "TorchNodeServer", lambda **kw: FakeNode()) node = su.run_startup( tracker_url="http://localhost:8080", model_id="Qwen/Qwen2.5-0.5B-Instruct", # shard_start and shard_end intentionally omitted quantization="bfloat16", host="127.0.0.1", ) assert calls == ["Qwen/Qwen2.5-0.5B-Instruct"] assert isinstance(node, FakeNode) def test_legacy_start_subcommand_accepted(monkeypatch): """meshnet-node start --tracker http://... does not crash on arg parsing.""" from meshnet_node.cli import main def fake_run_startup(*args, **kwargs): class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--tracker", "http://localhost:8080", "--model", "stub-model", "--port", "0", ]) raised = [] def fake_sleep(t): raise KeyboardInterrupt with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=fake_sleep): try: main() except SystemExit as exc: raised.append(exc.code) # Exited (either 0 or via KeyboardInterrupt caught in _cmd_start) # The important thing is no unhandled exception from arg parsing def test_legacy_start_treats_repo_model_as_model_id(monkeypatch): """`meshnet-node start --model org/repo` enters the HF model startup path.""" from meshnet_node.cli import main captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--tracker", "http://192.168.0.179:8081", "--model", "Qwen/Qwen2.5-0.5B-Instruct", "--port", "0", ]) with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 assert captured["model"] == "Qwen2.5-0.5B-Instruct" assert captured["model_id"] == "Qwen/Qwen2.5-0.5B-Instruct" def test_legacy_start_catalog_model_with_pinned_shards(monkeypatch): """Catalog model names accept --shard-start/--shard-end without --model-id.""" from meshnet_node.cli import main captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--tracker", "http://192.168.0.179:8080", "--model", "Qwen3.6-35B-A3B", "--shard-start", "0", "--shard-end", "44", "--port", "0", ]) with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 assert captured["model"] == "Qwen3.6-35B-A3B" assert captured["model_id"] is None assert captured["shard_start"] == 0 assert captured["shard_end"] == 44 def test_legacy_start_model_id_alias_for_catalog_name(monkeypatch): """--model-id with a catalog name routes through the tracker preset path.""" from meshnet_node.cli import main captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--tracker", "http://192.168.0.179:8080", "--model-id", "Qwen3.6-35B-A3B", "--port", "0", ]) with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 assert captured["model"] == "Qwen3.6-35B-A3B" assert captured["model_id"] is None def test_legacy_start_hf_repo_with_pinned_shards(monkeypatch): """HF repo --model with pinned shards still enters the torch startup path.""" from meshnet_node.cli import main captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--tracker", "http://192.168.0.179:8081", "--model", "Qwen/Qwen2.5-0.5B-Instruct", "--shard-start", "12", "--shard-end", "23", "--port", "0", ]) with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 assert captured["model"] == "Qwen2.5-0.5B-Instruct" assert captured["model_id"] == "Qwen/Qwen2.5-0.5B-Instruct" assert captured["shard_start"] == 12 assert captured["shard_end"] == 23 def test_legacy_start_falls_back_to_env_tracker_and_model(monkeypatch): """`meshnet-node start` uses env defaults when tracker/model flags are omitted.""" import importlib from meshnet_node import config as config_mod from meshnet_node.cli import main monkeypatch.setenv("MESHNET_TRACKER_URL", "http://env-tracker:8081") monkeypatch.setenv("MESHNET_MODEL", "Qwen/Qwen2.5-0.5B-Instruct") importlib.reload(config_mod) captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--port", "0", ]) try: with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 finally: monkeypatch.delenv("MESHNET_TRACKER_URL", raising=False) monkeypatch.delenv("MESHNET_MODEL", raising=False) importlib.reload(config_mod) assert captured["tracker_url"] == "http://env-tracker:8081" assert captured["model"] == "Qwen2.5-0.5B-Instruct" assert captured["model_id"] == "Qwen/Qwen2.5-0.5B-Instruct" def test_legacy_start_without_port_uses_next_available_port(monkeypatch): """Omitting --port skips an occupied default port before startup loads the model.""" from meshnet_node.cli import main captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() occupied = socket.socket(socket.AF_INET, socket.SOCK_STREAM) occupied.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) occupied.bind(("127.0.0.1", 7000)) occupied.listen(1) try: monkeypatch.setattr(sys, "argv", [ "meshnet-node", "start", "--tracker", "http://192.168.0.179:8081", "--model", "Qwen/Qwen2.5-0.5B-Instruct", "--host", "127.0.0.1", ]) with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("time.sleep", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 finally: occupied.close() assert captured["port"] == 7001 def test_default_cli_passes_advertise_host(monkeypatch): """The documented no-subcommand LAN flag reaches startup.""" from meshnet_node.cli import main saved = { "model_hf_repo": "Qwen/Qwen2.5-0.5B-Instruct", "model_name": "Qwen2.5-0.5B-Instruct", "quantization": "nf4", "tracker_url": "http://localhost:8080", "wallet_path": "", "download_dir": "", "port": 7000, "host": "0.0.0.0", } captured = {} def fake_run_startup(*args, **kwargs): captured.update(kwargs) class _FakeNode: chat_completion_count = 0 def stop(self): pass return _FakeNode() monkeypatch.setattr(sys, "argv", [ "meshnet-node", "--tracker", "http://192.168.0.179:8081", "--advertise-host", "192.168.0.42", "--debug", "--no-tui", ]) with patch("meshnet_node.config.load_config", return_value=saved): with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup): with patch("meshnet_node.dashboard.run_dashboard", side_effect=KeyboardInterrupt): try: main() except SystemExit as exc: assert exc.code == 0 assert captured["tracker_url"] == "http://192.168.0.179:8081" assert captured["advertise_host"] == "192.168.0.42" assert captured["debug"] is True