mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
565 lines
18 KiB
Python
565 lines
18 KiB
Python
"""Tests for the ``jarvis mine`` CLI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from openjarvis.cli import cli
|
|
from openjarvis.mining import Sidecar
|
|
|
|
|
|
def test_mine_help() -> None:
|
|
result = CliRunner().invoke(cli, ["mine", "--help"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Configure and run Pearl mining" in result.output
|
|
assert "init" in result.output
|
|
assert "start" in result.output
|
|
assert "doctor" in result.output
|
|
|
|
|
|
def test_mine_models_lists_validated_and_planned_models() -> None:
|
|
result = CliRunner().invoke(cli, ["mine", "models"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Pearl Mining Models" in result.output
|
|
assert "pearl-ai/Llama-3.3-70B-Instruct-pearl" in result.output
|
|
assert "pearl-ai/Gemma-4-31B-it-pearl" in result.output
|
|
assert "pearl-ai/Llama-3.1-8B-Instruct-pearl" in result.output
|
|
assert "Qwen" not in result.output
|
|
assert "validated" in result.output
|
|
assert "planned" in result.output
|
|
|
|
|
|
def test_pearl_base_model_lookup_uses_public_pearl_ai_artifacts():
|
|
from openjarvis.mining._models import pearl_variant_for_base_model
|
|
|
|
assert (
|
|
pearl_variant_for_base_model("google/gemma-4-31B-it")
|
|
== "pearl-ai/Gemma-4-31B-it-pearl"
|
|
)
|
|
assert pearl_variant_for_base_model("Qwen/Qwen3.5-9B") is None
|
|
assert pearl_variant_for_base_model("google/gemma-4-E4B-it") is None
|
|
|
|
|
|
def test_mine_inspect_model_validated_artifact_passes(monkeypatch) -> None:
|
|
model = "pearl-ai/Llama-3.3-70B-Instruct-pearl"
|
|
|
|
def fake_hf_json(path: str, *, token: str = "", timeout: float = 10.0):
|
|
if path.endswith("/tree/main?recursive=false"):
|
|
return [
|
|
{"path": "config.json"},
|
|
{"path": "tokenizer.json"},
|
|
{"path": "model.safetensors"},
|
|
]
|
|
return {
|
|
"config": {
|
|
"architectures": ["LlamaForCausalLM"],
|
|
"quantization_config": {"quant_method": "pearl"},
|
|
}
|
|
}
|
|
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd._hf_json", fake_hf_json)
|
|
|
|
result = CliRunner().invoke(cli, ["mine", "inspect-model", "--model", model])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Artifact inspection passed" in result.output
|
|
assert "Pearl quantization" in result.output
|
|
|
|
|
|
def test_mine_inspect_model_gemma4_requires_processor_metadata(monkeypatch) -> None:
|
|
model = "pearl-ai/Gemma-4-31B-it-pearl"
|
|
|
|
def fake_hf_json(path: str, *, token: str = "", timeout: float = 10.0):
|
|
if path.endswith("/tree/main?recursive=false"):
|
|
return [
|
|
{"path": "config.json"},
|
|
{"path": "processor_config.json"},
|
|
{"path": "tokenizer.json"},
|
|
{"path": "model.safetensors"},
|
|
]
|
|
return {
|
|
"config": {
|
|
"architectures": ["Gemma4ForConditionalGeneration"],
|
|
"quantization_config": {"quant_method": "pearl"},
|
|
}
|
|
}
|
|
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd._hf_json", fake_hf_json)
|
|
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
["mine", "inspect-model", "--model", model, "--allow-planned"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
assert "Gemma4 processor metadata" in result.output
|
|
assert "missing preprocessor_config.json" in result.output
|
|
|
|
|
|
def test_mine_inspect_model_accepts_local_artifact(tmp_path: Path) -> None:
|
|
artifact = tmp_path / "qwen-pearl"
|
|
artifact.mkdir()
|
|
(artifact / "config.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"architectures": ["Qwen3_5ForConditionalGeneration"],
|
|
"quantization_config": {"quant_method": "pearl"},
|
|
}
|
|
)
|
|
)
|
|
(artifact / "tokenizer.json").write_text("{}")
|
|
(artifact / "model.safetensors").write_text("placeholder")
|
|
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
["mine", "inspect-model", "--model", str(artifact)],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "local artifact" in result.output
|
|
assert "Artifact inspection passed" in result.output
|
|
|
|
|
|
def test_mine_init_writes_mining_config(tmp_path: Path) -> None:
|
|
config_path = tmp_path / "config.toml"
|
|
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"mine",
|
|
"init",
|
|
"--provider",
|
|
"cpu-pearl",
|
|
"--wallet-address",
|
|
"prl1qtestingaddr",
|
|
"--pearld-rpc-url",
|
|
"http://127.0.0.1:44107",
|
|
"--pearld-rpc-user",
|
|
"rpcuser",
|
|
"--pearld-rpc-password-env",
|
|
"TEST_PEARLD_PASSWORD",
|
|
],
|
|
env={"OPENJARVIS_CONFIG": str(config_path)},
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
content = config_path.read_text()
|
|
assert "[mining]" in content
|
|
assert 'provider = "cpu-pearl"' in content
|
|
assert 'wallet_address = "prl1qtestingaddr"' in content
|
|
assert 'pearld_rpc_password_env = "TEST_PEARLD_PASSWORD"' in content
|
|
|
|
|
|
def test_mine_init_writes_cuda_visible_devices_for_vllm(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
from openjarvis.mining._stubs import MiningCapabilities
|
|
|
|
config_path = tmp_path / "config.toml"
|
|
|
|
with (
|
|
patch("openjarvis.cli.mine_cmd._detect_hardware"),
|
|
patch(
|
|
"openjarvis.cli.mine_cmd.detect_for_engine_model",
|
|
return_value=MiningCapabilities(supported=True),
|
|
),
|
|
patch(
|
|
"openjarvis.cli.mine_cmd.check_docker_available",
|
|
return_value=(True, ""),
|
|
),
|
|
patch("openjarvis.cli.mine_cmd.check_disk_free", return_value=(True, "")),
|
|
patch("openjarvis.cli.mine_cmd._docker_from_env", return_value=MagicMock()),
|
|
patch(
|
|
"openjarvis.cli.mine_cmd.PearlDockerLauncher.ensure_image",
|
|
return_value="openjarvis/pearl-miner:master",
|
|
),
|
|
):
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"mine",
|
|
"init",
|
|
"--provider",
|
|
"vllm-pearl",
|
|
"--wallet-address",
|
|
"prl1qtestingaddr",
|
|
"--pearld-rpc-url",
|
|
"http://127.0.0.1:44107",
|
|
"--pearld-rpc-user",
|
|
"rpcuser",
|
|
"--pearld-rpc-password-env",
|
|
"TEST_PEARLD_PASSWORD",
|
|
"--cuda-visible-devices",
|
|
"0,1",
|
|
],
|
|
env={"OPENJARVIS_CONFIG": str(config_path)},
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
content = config_path.read_text()
|
|
assert 'provider = "vllm-pearl"' in content
|
|
assert 'cuda_visible_devices = "0,1"' in content
|
|
|
|
|
|
def test_mine_init_writes_local_model_path_for_vllm(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config_path = tmp_path / "config.toml"
|
|
local_model = tmp_path / "converted-qwen"
|
|
local_model.mkdir()
|
|
|
|
with (
|
|
patch("openjarvis.cli.mine_cmd._detect_hardware"),
|
|
patch(
|
|
"openjarvis.cli.mine_cmd.check_docker_available",
|
|
return_value=(True, ""),
|
|
),
|
|
patch("openjarvis.cli.mine_cmd.check_disk_free", return_value=(True, "")),
|
|
patch("openjarvis.cli.mine_cmd._docker_from_env", return_value=MagicMock()),
|
|
patch(
|
|
"openjarvis.cli.mine_cmd.PearlDockerLauncher.ensure_image",
|
|
return_value="openjarvis/pearl-miner:master",
|
|
),
|
|
):
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"mine",
|
|
"init",
|
|
"--provider",
|
|
"vllm-pearl",
|
|
"--wallet-address",
|
|
"prl1qtestingaddr",
|
|
"--pearld-rpc-url",
|
|
"http://127.0.0.1:44107",
|
|
"--pearld-rpc-user",
|
|
"rpcuser",
|
|
"--pearld-rpc-password-env",
|
|
"TEST_PEARLD_PASSWORD",
|
|
"--model",
|
|
"pearl-ai/Llama-3.1-8B-Instruct-pearl",
|
|
"--local-model-path",
|
|
str(local_model),
|
|
"--vllm-arg=--language-model-only",
|
|
"--vllm-arg=--skip-mm-profiling",
|
|
],
|
|
env={"OPENJARVIS_CONFIG": str(config_path)},
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
content = config_path.read_text()
|
|
assert 'model = "pearl-ai/Llama-3.1-8B-Instruct-pearl"' in content
|
|
assert f'local_model_path = "{local_model.resolve()}"' in content
|
|
assert 'vllm_args = ["--language-model-only", "--skip-mm-profiling"]' in content
|
|
|
|
|
|
def test_mine_start_uses_configured_provider(tmp_path: Path, monkeypatch) -> None:
|
|
config_path = tmp_path / "config.toml"
|
|
config_path.write_text(
|
|
"""
|
|
[mining]
|
|
provider = "cpu-pearl"
|
|
wallet_address = "prl1qtest"
|
|
submit_target = "solo"
|
|
|
|
[mining.extra]
|
|
pearld_rpc_url = "http://127.0.0.1:44107"
|
|
pearld_rpc_user = "rpcuser"
|
|
pearld_rpc_password_env = "TEST_PEARLD_PASSWORD"
|
|
gateway_host = "127.0.0.1"
|
|
gateway_port = 18337
|
|
metrics_port = 19109
|
|
"""
|
|
)
|
|
sidecar_path = tmp_path / "mining.json"
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
|
|
started_configs = []
|
|
|
|
async def fake_start(config):
|
|
started_configs.append(config)
|
|
|
|
fake_provider = MagicMock()
|
|
provider_cls = MagicMock(return_value=fake_provider)
|
|
fake_provider.start = fake_start
|
|
|
|
with patch("openjarvis.cli.mine_cmd._provider_ids", return_value=("cpu-pearl",)):
|
|
with patch("openjarvis.cli.mine_cmd.MinerRegistry.contains", return_value=True):
|
|
with patch(
|
|
"openjarvis.cli.mine_cmd.MinerRegistry.get",
|
|
return_value=provider_cls,
|
|
):
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
["mine", "start"],
|
|
env={
|
|
"OPENJARVIS_CONFIG": str(config_path),
|
|
"TEST_PEARLD_PASSWORD": "secret",
|
|
},
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Started" in result.output
|
|
provider_cls.assert_called_once_with()
|
|
assert started_configs[0].provider == "cpu-pearl"
|
|
|
|
|
|
def test_mine_status_reports_sidecar_and_metrics(tmp_path: Path, monkeypatch) -> None:
|
|
sidecar_path = tmp_path / "mining.json"
|
|
Sidecar.write(
|
|
sidecar_path,
|
|
{
|
|
"provider": "cpu-pearl",
|
|
"wallet_address": "prl1qtest",
|
|
"gateway_url": "http://127.0.0.1:8337",
|
|
"metrics_url": "http://127.0.0.1:9109/metrics",
|
|
"gateway_pid": 111,
|
|
"miner_loop_pid": 222,
|
|
},
|
|
)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd._pid_alive", lambda pid: True)
|
|
|
|
stats = MagicMock()
|
|
stats.shares_submitted = 3
|
|
stats.shares_accepted = 2
|
|
stats.blocks_found = 1
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._stats_from_metrics_url",
|
|
lambda url, provider_id: (stats, None),
|
|
)
|
|
|
|
result = CliRunner().invoke(cli, ["mine", "status"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "cpu-pearl" in result.output
|
|
assert "Shares submitted" in result.output
|
|
assert "3" in result.output
|
|
|
|
|
|
def test_mine_stop_terminates_pids_and_removes_sidecar(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
sidecar_path = tmp_path / "mining.json"
|
|
Sidecar.write(
|
|
sidecar_path,
|
|
{
|
|
"provider": "cpu-pearl",
|
|
"gateway_pid": 111,
|
|
"miner_loop_pid": 222,
|
|
},
|
|
)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
terminated: list[int] = []
|
|
|
|
def fake_terminate(pid, *, grace_seconds):
|
|
terminated.append(pid)
|
|
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd._terminate_pid", fake_terminate)
|
|
|
|
result = CliRunner().invoke(cli, ["mine", "stop"])
|
|
|
|
assert result.exit_code == 0
|
|
assert sorted(terminated) == [111, 222]
|
|
assert not sidecar_path.exists()
|
|
|
|
|
|
def test_mine_doctor_without_config(tmp_path: Path, monkeypatch) -> None:
|
|
config_path = tmp_path / "missing.toml"
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", tmp_path / "none.json")
|
|
|
|
with patch("openjarvis.cli.mine_cmd._provider_ids", return_value=()):
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
["mine", "doctor"],
|
|
env={"OPENJARVIS_CONFIG": str(config_path)},
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Pearl Mining Doctor" in result.output
|
|
assert "jarvis mine init" in result.output
|
|
|
|
|
|
def test_mine_status_no_session(tmp_path: Path, monkeypatch) -> None:
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", tmp_path / "none.json")
|
|
|
|
result = CliRunner().invoke(cli, ["mine", "status"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No active mining session" in result.output
|
|
|
|
|
|
def test_mine_validate_model_blocks_planned_without_allow(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
sidecar_path = tmp_path / "mining.json"
|
|
model = "pearl-ai/Gemma-4-31B-it-pearl"
|
|
Sidecar.write(
|
|
sidecar_path,
|
|
{
|
|
"provider": "vllm-pearl",
|
|
"model": model,
|
|
"vllm_endpoint": "http://127.0.0.1:8000/v1",
|
|
"gateway_metrics_url": "http://127.0.0.1:8339",
|
|
},
|
|
)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._get_json",
|
|
lambda url, *, timeout: {"data": [{"id": model}]},
|
|
)
|
|
stats = MagicMock()
|
|
stats.shares_submitted = 1
|
|
stats.shares_accepted = 1
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._stats_from_metrics_url",
|
|
lambda url, provider_id: (stats, None),
|
|
)
|
|
|
|
result = CliRunner().invoke(cli, ["mine", "validate-model"])
|
|
|
|
assert result.exit_code != 0
|
|
assert "planned" in result.output
|
|
assert "validation checks failed" in result.output.lower()
|
|
|
|
|
|
def test_mine_validate_model_allows_planned_with_runtime_evidence(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
sidecar_path = tmp_path / "mining.json"
|
|
model = "pearl-ai/Gemma-4-31B-it-pearl"
|
|
Sidecar.write(
|
|
sidecar_path,
|
|
{
|
|
"provider": "vllm-pearl",
|
|
"model": model,
|
|
"vllm_endpoint": "http://127.0.0.1:8000/v1",
|
|
"gateway_metrics_url": "http://127.0.0.1:8339",
|
|
},
|
|
)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._get_json",
|
|
lambda url, *, timeout: {"data": [{"id": model}]},
|
|
)
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._post_json",
|
|
lambda url, payload, *, timeout: {"choices": [{"message": {"content": "ok"}}]},
|
|
)
|
|
stats = MagicMock()
|
|
stats.shares_submitted = 2
|
|
stats.shares_accepted = 1
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._stats_from_metrics_url",
|
|
lambda url, provider_id: (stats, None),
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"mine",
|
|
"validate-model",
|
|
"--allow-planned",
|
|
"--prompt",
|
|
"hello",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Validation checks passed" in result.output
|
|
assert "Chat completion" in result.output
|
|
assert "submitted=2 accepted=1" in result.output
|
|
|
|
|
|
def test_mine_validate_model_writes_json_artifact(tmp_path: Path, monkeypatch) -> None:
|
|
sidecar_path = tmp_path / "mining.json"
|
|
output_path = tmp_path / "artifacts" / "qwen-validation.json"
|
|
model = "pearl-ai/Gemma-4-31B-it-pearl"
|
|
Sidecar.write(
|
|
sidecar_path,
|
|
{
|
|
"provider": "vllm-pearl",
|
|
"model": model,
|
|
"vllm_endpoint": "http://127.0.0.1:8000/v1",
|
|
"gateway_metrics_url": "http://127.0.0.1:8339",
|
|
},
|
|
)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._get_json",
|
|
lambda url, *, timeout: {"data": [{"id": model}]},
|
|
)
|
|
stats = MagicMock()
|
|
stats.shares_submitted = 2
|
|
stats.shares_accepted = 1
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._stats_from_metrics_url",
|
|
lambda url, provider_id: (stats, None),
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"mine",
|
|
"validate-model",
|
|
"--allow-planned",
|
|
"--output",
|
|
str(output_path),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(output_path.read_text())
|
|
assert payload["schema_version"] == 1
|
|
assert payload["model"] == model
|
|
assert payload["status"] == "passed"
|
|
assert payload["prompt_ran"] is False
|
|
assert {check["name"] for check in payload["checks"]} >= {
|
|
"Model registry",
|
|
"Sidecar",
|
|
"vLLM /models",
|
|
"Gateway metrics",
|
|
}
|
|
|
|
|
|
def test_mine_validate_model_falls_back_to_vllm_metrics(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
sidecar_path = tmp_path / "mining.json"
|
|
model = "pearl-ai/Llama-3.3-70B-Instruct-pearl"
|
|
Sidecar.write(
|
|
sidecar_path,
|
|
{
|
|
"provider": "vllm-pearl",
|
|
"model": model,
|
|
"vllm_endpoint": "http://127.0.0.1:8000/v1",
|
|
"gateway_metrics_url": "http://127.0.0.1:8339",
|
|
},
|
|
)
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd.SIDECAR_PATH", sidecar_path)
|
|
monkeypatch.setattr(
|
|
"openjarvis.cli.mine_cmd._get_json",
|
|
lambda url, *, timeout: {"data": [{"id": model}]},
|
|
)
|
|
stats = MagicMock()
|
|
stats.shares_submitted = 0
|
|
stats.shares_accepted = 0
|
|
|
|
def fake_stats(url, provider_id):
|
|
if url == "http://127.0.0.1:8339/metrics":
|
|
return None, "connection refused"
|
|
return stats, None
|
|
|
|
monkeypatch.setattr("openjarvis.cli.mine_cmd._stats_from_metrics_url", fake_stats)
|
|
|
|
result = CliRunner().invoke(cli, ["mine", "validate-model"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "vLLM submitted=0 accepted=0" in result.output
|