From 31efd9e672cc76f5e8dd9094880a3327b6975c7c Mon Sep 17 00:00:00 2001 From: IsaacH Date: Thu, 30 Apr 2026 11:06:29 +0800 Subject: [PATCH] fix: detect total RAM on Windows via GlobalMemoryStatusEx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_total_ram_gb()` had branches for Darwin (sysctl) and Linux (/proc/meminfo) but no Windows path, so `jarvis init` reported "0.0 GB RAM" on every Windows host. The downstream `recommend_model()` then fell back to VRAM-only sizing, often selecting a smaller tier than the system can actually run. Add a Windows branch that calls `GlobalMemoryStatusEx` via ctypes — no new dependency. Add a platform-skip-aware test for each OS branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/openjarvis/core/config.py | 23 ++++++++++++++++++- tests/hardware/test_hardware_profiles.py | 29 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/openjarvis/core/config.py b/src/openjarvis/core/config.py index 10aafd22..7458f6f8 100644 --- a/src/openjarvis/core/config.py +++ b/src/openjarvis/core/config.py @@ -188,13 +188,34 @@ def _total_ram_gb() -> float: if platform.system() == "Darwin": raw = _run_cmd(["sysctl", "-n", "hw.memsize"]) return round(int(raw) / (1024**3), 1) if raw else 0.0 + if platform.system() == "Windows": + import ctypes + + class _MemoryStatusEx(ctypes.Structure): + _fields_ = [ + ("dwLength", ctypes.c_ulong), + ("dwMemoryLoad", ctypes.c_ulong), + ("ullTotalPhys", ctypes.c_ulonglong), + ("ullAvailPhys", ctypes.c_ulonglong), + ("ullTotalPageFile", ctypes.c_ulonglong), + ("ullAvailPageFile", ctypes.c_ulonglong), + ("ullTotalVirtual", ctypes.c_ulonglong), + ("ullAvailVirtual", ctypes.c_ulonglong), + ("sullAvailExtendedVirtual", ctypes.c_ulonglong), + ] + + stat = _MemoryStatusEx() + stat.dwLength = ctypes.sizeof(_MemoryStatusEx) + if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)): + return round(stat.ullTotalPhys / (1024**3), 1) + return 0.0 meminfo = Path("/proc/meminfo") if meminfo.exists(): for line in meminfo.read_text().splitlines(): if line.startswith("MemTotal"): kb = int(line.split()[1]) return round(kb / (1024**2), 1) - except (OSError, ValueError): + except (OSError, ValueError, AttributeError): pass return 0.0 diff --git a/tests/hardware/test_hardware_profiles.py b/tests/hardware/test_hardware_profiles.py index d1686931..d35740a8 100644 --- a/tests/hardware/test_hardware_profiles.py +++ b/tests/hardware/test_hardware_profiles.py @@ -3,13 +3,17 @@ and engine recommendation.""" from __future__ import annotations +import sys from unittest.mock import patch +import pytest + from openjarvis.core.config import ( GpuInfo, _detect_amd_gpu, _detect_apple_gpu, _detect_nvidia_gpu, + _total_ram_gb, recommend_engine, ) @@ -75,6 +79,31 @@ class TestDetectHardware: assert _detect_apple_gpu() is None +# --------------------------------------------------------------------------- +# RAM detection +# --------------------------------------------------------------------------- + + +class TestTotalRamGb: + """Tests for _total_ram_gb() across platforms.""" + + @pytest.mark.skipif(sys.platform != "win32", reason="Requires Windows") + def test_total_ram_gb_windows(self): + """On Windows, GlobalMemoryStatusEx must return a positive RAM value.""" + ram = _total_ram_gb() + assert ram > 0, f"Expected > 0 GB on Windows, got {ram}" + + @pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") + def test_total_ram_gb_darwin(self): + ram = _total_ram_gb() + assert ram > 0, f"Expected > 0 GB on macOS, got {ram}" + + @pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Requires Linux") + def test_total_ram_gb_linux(self): + ram = _total_ram_gb() + assert ram > 0, f"Expected > 0 GB on Linux, got {ram}" + + # --------------------------------------------------------------------------- # Engine recommendation # ---------------------------------------------------------------------------