Files
mlx-knife/tests_2.0/test_json_api_list.py
The BROKE Cluster Team 5751545b8b Release 2.0.4-beta.7: Server robustness + Vision per-chunk streaming
- Server: exit codes, /v1/models crash fix, vision routing, MLXK2_MAX_TOKENS
- Vision: true SSE streaming, hallucination fix (local numbering)
- Workspace: list prefix-match, push ambiguous pattern handling
- Docs: SERVER-HANDBOOK accuracy updates

See CHANGELOG.md for details.
2026-01-18 16:57:32 +01:00

357 lines
12 KiB
Python

"""Tests for JSON API spec v0.1.2: list operation minimal model object.
Covers: size_bytes, last_modified (ISO-8601 Z), framework, model_type,
capabilities, health, hash selection, cached.
"""
from datetime import datetime
from typing import Set
import pytest
from mlxk2.operations.list import list_models
def _is_iso_utc_z(ts: str) -> bool:
try:
# Must end with 'Z' and be parseable
if not ts.endswith("Z"):
return False
# Strip Z, attempt parsing
datetime.fromisoformat(ts.replace("Z", ""))
return True
except Exception:
return False
@pytest.mark.spec
def test_list_minimal_model_object_fields(mock_models, isolated_cache):
"""Each model entry returns the minimal model object with health."""
result = list_models()
assert result["status"] == "success"
assert result["command"] == "list"
models = result["data"]["models"]
assert isinstance(models, list)
assert result["data"]["count"] == len(models)
# Allowed enums
allowed_framework: Set[str] = {"MLX", "GGUF", "PyTorch", "Unknown"}
allowed_model_types: Set[str] = {"chat", "embedding", "base", "unknown"}
# Verify minimal fields and types
for m in models:
# Required fields
assert set([
"name", "hash", "size_bytes", "last_modified", "framework",
"model_type", "capabilities", "health", "cached"
]).issubset(m.keys())
assert isinstance(m["name"], str) and "/" in m["name"]
# hash: 40-char or None
h = m["hash"]
assert (h is None) or (isinstance(h, str) and len(h) == 40)
# size_bytes integer >= 0
assert isinstance(m["size_bytes"], int)
assert m["size_bytes"] >= 0
# last_modified as ISO-8601 UTC Z
assert isinstance(m["last_modified"], str)
assert _is_iso_utc_z(m["last_modified"]) is True
# framework
assert m["framework"] in allowed_framework
# model_type + capabilities
assert m["model_type"] in allowed_model_types
assert isinstance(m["capabilities"], list)
# health
assert m["health"] in {"healthy", "unhealthy"}
# cached flag
assert m["cached"] is True
# Spec 0.1.2: no human-readable size; ensure we do not expose 'size' or internal paths
assert "size" not in m
assert "hashes" not in m
@pytest.mark.spec
def test_list_pattern_filter_case_insensitive(mock_models, isolated_cache):
"""Pattern filters case-insensitively on model name."""
result = list_models(pattern="llama")
models = result["data"]["models"]
assert all("llama" in m["name"].lower() for m in models)
# A different pattern should yield different subset
result_q = list_models(pattern="Qwen")
models_q = result_q["data"]["models"]
assert all("qwen" in m["name"].lower() for m in models_q)
# Ensure partition is non-trivial in our fixture
assert set(m["name"].lower() for m in models).isdisjoint(
set(m["name"].lower() for m in models_q)
) is True
@pytest.mark.spec
def test_list_empty_cache(isolated_cache):
"""Empty cache yields empty list and count 0."""
# Remove all models (keep canary)
for d in isolated_cache.iterdir():
if d.is_dir() and d.name.startswith("models--") and "TEST-CACHE-SENTINEL" not in d.name:
# Safe in tests; strict delete is enforced by fixture env var
from shutil import rmtree
rmtree(d)
result = list_models()
assert result["status"] == "success"
assert result["data"]["models"] == []
assert result["data"]["count"] == 0
class TestListWorkspacePrefix:
"""Test list_models() with workspace path patterns (Session 103)."""
def test_list_workspace_exact_match(self, tmp_path):
"""Test list_models with exact workspace path."""
import os
ws = tmp_path / "my-model"
ws.mkdir()
(ws / "config.json").write_text('{"model_type": "llama"}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./my-model")
assert result["status"] == "success"
assert result["data"]["count"] == 1
assert len(result["data"]["models"]) == 1
assert "my-model" in result["data"]["models"][0]["name"]
finally:
os.chdir(old_cwd)
def test_list_workspace_prefix_match(self, tmp_path):
"""Test list_models with workspace prefix pattern."""
import os
# Create multiple workspaces with common prefix
for name in ["gemma-3n-4bit", "gemma-3n-FIXED-4bit", "gemma-3n-8bit"]:
ws = tmp_path / name
ws.mkdir()
(ws / "config.json").write_text('{"model_type": "gemma"}')
# Create non-matching workspace
other = tmp_path / "llama-3"
other.mkdir()
(other / "config.json").write_text('{"model_type": "llama"}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./gemma-")
assert result["status"] == "success"
assert result["data"]["count"] == 3
# All matches should contain gemma
for m in result["data"]["models"]:
assert "gemma" in m["name"]
finally:
os.chdir(old_cwd)
def test_list_workspace_prefix_no_match(self, tmp_path):
"""Test list_models with non-matching prefix returns empty list."""
import os
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./nonexistent-")
assert result["status"] == "success"
assert result["data"]["count"] == 0
assert result["data"]["models"] == []
finally:
os.chdir(old_cwd)
def test_list_workspace_does_not_fall_through_to_cache(self, tmp_path, isolated_cache):
"""Test explicit path patterns don't fall through to cache search."""
import os
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
# ./gemma- should NOT match cache models even if they contain "gemma"
# It should only search local workspaces
result = list_models(pattern="./gemma-")
assert result["status"] == "success"
# Should be empty (no local workspaces) not cache models
assert result["data"]["count"] == 0
finally:
os.chdir(old_cwd)
def test_list_workspace_sorted_by_name(self, tmp_path):
"""Test workspace results are sorted by name."""
import os
for name in ["model-c", "model-a", "model-b"]:
ws = tmp_path / name
ws.mkdir()
(ws / "config.json").write_text('{}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./model-")
names = [m["name"] for m in result["data"]["models"]]
# Should be sorted (names contain full path, but sorted by name)
assert "model-a" in names[0]
assert "model-b" in names[1]
assert "model-c" in names[2]
finally:
os.chdir(old_cwd)
def test_list_workspace_absolute_path(self, tmp_path):
"""Test list_models with absolute workspace path."""
ws = tmp_path / "model"
ws.mkdir()
(ws / "config.json").write_text('{}')
result = list_models(pattern=str(ws))
assert result["status"] == "success"
assert result["data"]["count"] == 1
def test_list_workspace_has_all_fields(self, tmp_path):
"""Test workspace model objects have all required fields."""
import os
ws = tmp_path / "my-model"
ws.mkdir()
(ws / "config.json").write_text('{"model_type": "llama"}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./my-model")
m = result["data"]["models"][0]
# Check required fields exist
required = ["name", "hash", "size_bytes", "last_modified",
"framework", "model_type", "capabilities", "health", "cached"]
for field in required:
assert field in m, f"Missing field: {field}"
# Workspace-specific: hash should be None, cached should be False
assert m["hash"] is None
assert m["cached"] is False
finally:
os.chdir(old_cwd)
def test_list_workspace_display_name_relative(self, tmp_path):
"""Test display_name is relative for relative input patterns."""
import os
ws = tmp_path / "my-model"
ws.mkdir()
(ws / "config.json").write_text('{}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./my-model")
m = result["data"]["models"][0]
# name should be absolute (for programmatic use)
assert m["name"].startswith("/")
# display_name should be relative
assert m["display_name"] == "my-model"
finally:
os.chdir(old_cwd)
def test_list_workspace_display_name_absolute(self, tmp_path):
"""Test display_name is absolute for absolute input patterns."""
ws = tmp_path / "my-model"
ws.mkdir()
(ws / "config.json").write_text('{}')
# Use absolute path pattern
result = list_models(pattern=str(ws))
m = result["data"]["models"][0]
# Both name and display_name should be absolute
assert m["name"].startswith("/")
assert m["display_name"].startswith("/")
assert m["display_name"] == str(ws)
def test_list_workspace_display_name_prefix_match(self, tmp_path):
"""Test display_name works correctly with prefix matching."""
import os
for name in ["gemma-a", "gemma-b"]:
ws = tmp_path / name
ws.mkdir()
(ws / "config.json").write_text('{}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern="./gemma-")
# Both should have relative display_name
display_names = [m["display_name"] for m in result["data"]["models"]]
assert "gemma-a" in display_names
assert "gemma-b" in display_names
# None should be absolute
assert not any(dn.startswith("/") for dn in display_names)
finally:
os.chdir(old_cwd)
def test_list_workspace_directory_scan(self, tmp_path):
"""Test listing all workspaces in a directory (. pattern)."""
import os
# Create multiple workspaces
for name in ["model-a", "model-b"]:
ws = tmp_path / name
ws.mkdir()
(ws / "config.json").write_text('{}')
old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = list_models(pattern=".")
assert result["status"] == "success"
assert result["data"]["count"] == 2
# display_names should be just the directory names
display_names = [m["display_name"] for m in result["data"]["models"]]
assert "model-a" in display_names
assert "model-b" in display_names
finally:
os.chdir(old_cwd)
def test_list_workspace_directory_scan_absolute(self, tmp_path):
"""Test listing all workspaces with absolute directory path."""
# Create workspaces
for name in ["model-a", "model-b"]:
ws = tmp_path / name
ws.mkdir()
(ws / "config.json").write_text('{}')
# Use absolute path
result = list_models(pattern=str(tmp_path))
assert result["status"] == "success"
assert result["data"]["count"] == 2
# display_names should be absolute (because input was absolute)
for m in result["data"]["models"]:
assert m["display_name"].startswith("/")
# name should also be absolute
assert m["name"].startswith("/")