"""US-004 integration tests: node self-configuring startup sequence.""" import json import sys import types import urllib.request from pathlib import Path import pytest from meshnet_node.downloader import download_shard from meshnet_node.hardware import detect_hardware from meshnet_node.startup import run_startup from meshnet_node.wallet import _b58encode, load_or_create_wallet from meshnet_tracker.server import TrackerServer # --------------------------------------------------------------------------- # Unit tests — hardware, wallet, downloader # --------------------------------------------------------------------------- def test_detect_hardware_returns_valid_profile(): """Hardware detection always returns a dict with required keys.""" hw = detect_hardware() assert hw["device"] in {"cuda", "cpu"} assert isinstance(hw.get("vram_mb"), int) if hw["device"] == "cpu": assert hw["gpu_name"] is None assert hw["vram_mb"] == 0 else: assert isinstance(hw["gpu_name"], str) and hw["gpu_name"] assert hw["vram_mb"] > 0 def test_wallet_generates_new_keypair(tmp_path): """A new wallet is created when none exists, saved to disk.""" wallet_file = tmp_path / "wallet.json" assert not wallet_file.exists() secret, public, address = load_or_create_wallet(path=wallet_file) assert wallet_file.exists() assert wallet_file.stat().st_mode & 0o777 == 0o600 assert len(secret) == 32 assert len(public) == 32 assert len(address) > 20 # base58-encoded 32 bytes is 43-44 chars typically # Solana addresses are base58 — no '0', 'O', 'I', 'l' for ch in "0OIl": assert ch not in address, f"Invalid base58 char {ch!r} in address {address!r}" def test_wallet_loads_existing_keypair(tmp_path): """Loading the same wallet file twice returns identical keys and address.""" wallet_file = tmp_path / "wallet.json" secret1, public1, address1 = load_or_create_wallet(path=wallet_file) secret2, public2, address2 = load_or_create_wallet(path=wallet_file) assert secret1 == secret2 assert public1 == public2 assert address1 == address2 def test_wallet_load_repairs_insecure_permissions(tmp_path): """Existing private key files are tightened to owner-only permissions.""" wallet_file = tmp_path / "wallet.json" load_or_create_wallet(path=wallet_file) wallet_file.chmod(0o644) load_or_create_wallet(path=wallet_file) assert wallet_file.stat().st_mode & 0o777 == 0o600 def test_base58_counts_only_leading_zero_bytes(): """Zero bytes inside the public key do not become extra base58 leading ones.""" assert _b58encode(bytes([0, 1, 0])) == "15R" def test_download_shard_stub_creates_cache(tmp_path): """Stub-model shard creates a local cache file without network access.""" shard_dir = download_shard("stub-model", 0, 31, cache_dir=tmp_path) assert shard_dir.exists() weights = shard_dir / "weights.json" assert weights.exists() data = json.loads(weights.read_text()) assert data["stub"] is True assert data["shard_start"] == 0 assert data["shard_end"] == 31 def test_download_shard_uses_huggingface_when_repo_is_assigned(tmp_path, monkeypatch): """Non-stub shards use the HuggingFace snapshot_download path.""" calls = [] def fake_snapshot_download(repo_id, cache_dir, local_dir): calls.append({"repo_id": repo_id, "cache_dir": cache_dir, "local_dir": local_dir}) Path(local_dir).mkdir(parents=True, exist_ok=True) return local_dir monkeypatch.setitem( sys.modules, "huggingface_hub", types.SimpleNamespace(snapshot_download=fake_snapshot_download), ) shard_dir = download_shard( "tiny-llama", 0, 3, cache_dir=tmp_path, hf_repo="org/tiny-llama-shards", progress=False, ) assert shard_dir == tmp_path / "tiny-llama" / "layers_0-3" assert calls == [{ "repo_id": "org/tiny-llama-shards", "cache_dir": str(tmp_path), "local_dir": str(shard_dir), }] def test_download_shard_stub_idempotent(tmp_path): """Calling download_shard twice does not error — file already exists.""" download_shard("stub-model", 0, 31, cache_dir=tmp_path, progress=False) shard_dir = download_shard("stub-model", 0, 31, cache_dir=tmp_path, progress=False) assert shard_dir.exists() # --------------------------------------------------------------------------- # Tracker assign endpoint # --------------------------------------------------------------------------- def _get_json(url: str) -> dict: with urllib.request.urlopen(url, timeout=5) as r: return json.loads(r.read()) def test_tracker_assign_returns_shard_for_empty_registry(): """Tracker assigns the full layer range when no nodes are registered.""" tracker = TrackerServer() port = tracker.start() try: resp = _get_json( f"http://127.0.0.1:{port}/v1/nodes/assign?model=stub-model&device=cpu&vram_mb=0" ) assert resp["shard_start"] == 0 assert resp["shard_end"] == 15 assert resp["model"] == "stub-model" assert resp["model_layers_end"] == 31 finally: tracker.stop() def test_tracker_assign_fills_gap(): """Tracker assigns the first uncovered layer range when a node is already registered.""" import json as _json import urllib.request as _ur tracker = TrackerServer() port = tracker.start() try: # Register a node covering layers 0-15 data = _json.dumps({ "endpoint": "http://127.0.0.1:9100", "shard_start": 0, "shard_end": 15, "hardware_profile": {}, "score": 1.0, }).encode() req = _ur.Request( f"http://127.0.0.1:{port}/v1/nodes/register", data=data, headers={"Content-Type": "application/json"}, method="POST", ) with _ur.urlopen(req) as r: r.read() # Assignment should fill the gap: layers 16-31 resp = _get_json( f"http://127.0.0.1:{port}/v1/nodes/assign?model=stub-model&device=cpu&vram_mb=0" ) assert resp["shard_start"] == 16 assert resp["shard_end"] == 31 finally: tracker.stop() def test_tracker_assign_returns_huggingface_repo_when_configured(): """Tracker includes the HuggingFace repo identifier in shard assignments.""" tracker = TrackerServer(model_presets={ "tiny-llama": {"layers_start": 0, "layers_end": 7, "hf_repo": "org/tiny-llama-shards"} }) port = tracker.start() try: resp = _get_json( f"http://127.0.0.1:{port}/v1/nodes/assign?model=tiny-llama&device=cuda&vram_mb=24576" ) assert resp["model"] == "tiny-llama" assert resp["hf_repo"] == "org/tiny-llama-shards" assert resp["shard_start"] == 0 assert resp["shard_end"] == 7 finally: tracker.stop() # --------------------------------------------------------------------------- # Full startup integration test # --------------------------------------------------------------------------- def test_full_startup_sequence(tmp_path): """Full startup: hardware → wallet → assign → download → start → register.""" tracker = TrackerServer(model_presets={"stub-model": {"layers_start": 0, "layers_end": 15}}) tracker_port = tracker.start() tracker_url = f"http://127.0.0.1:{tracker_port}" wallet_path = tmp_path / "wallet.json" cache_dir = tmp_path / "shards" try: node = run_startup( tracker_url=tracker_url, model="stub-model", wallet_path=wallet_path, cache_dir=cache_dir, ) try: # Wallet was created on disk assert wallet_path.exists() wallet_data = json.loads(wallet_path.read_text()) assert len(wallet_data) == 64 # Shard was cached assert any(cache_dir.rglob("weights.json")) # Node appears in tracker registry (route resolves successfully) route_resp = _get_json(f"{tracker_url}/v1/route?model=stub-model") assert len(route_resp["route"]) >= 1 assert node.port is not None assert any(str(node.port) in ep for ep in route_resp["route"]) # Node accepts an inference request payload = json.dumps({ "messages": [{"role": "user", "content": "hello"}], }).encode() req = urllib.request.Request( f"http://127.0.0.1:{node.port}/v1/infer", data=payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5) as resp: assert resp.status == 200 body = json.loads(resp.read()) # Last-shard node returns text; first-shard returns activations assert "text" in body or "activations" in body finally: node.stop() finally: tracker.stop() def test_startup_cpu_fallback(tmp_path, monkeypatch): """Node starts with CPU warning when no GPU is detected.""" import meshnet_node.startup as startup_mod monkeypatch.setattr( startup_mod, "detect_hardware", lambda: {"device": "cpu", "gpu_name": None, "vram_mb": 0}, ) tracker = TrackerServer(model_presets={"stub-model": {"layers_start": 0, "layers_end": 15}}) tracker_port = tracker.start() tracker_url = f"http://127.0.0.1:{tracker_port}" try: node = run_startup( tracker_url=tracker_url, model="stub-model", wallet_path=tmp_path / "wallet.json", cache_dir=tmp_path / "shards", ) try: # Node is running even on CPU assert node.port is not None route_resp = _get_json(f"{tracker_url}/v1/route?model=stub-model") assert len(route_resp["route"]) >= 1 finally: node.stop() finally: tracker.stop()