mindXtrain / tests /test_bankml_push.py
Gregory-L's picture
bankml: the verified CPU engine as backend, serve target and imprint probe (#1)
730c5bb
Raw History Blame Contribute Delete
14.9 kB
"""push_to_bankml: the Modelfile subset, the capability probe, and `bankml create` — no binary.
subprocess.run and shutil.which are monkeypatched; the LoRA merge is replaced by a stub.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
from typing import Any
import pytest
from typer.testing import CliRunner
from mindxtrain.deploy import bankml_push as BP
from mindxtrain.deploy.modelfile import ModelfileSpec, render_modelfile
SHA = "6b64c748d96ad26fd72402299bd27b2ae82f489bd0469498dd18eb6054058266"
DIGEST = "c" * 64
USAGE_035 = (
"usage: bankml usage [PID …]\n"
" bankml create NAME -f Modelfile [--registry DIR] [--models DIR]\n"
" bankml convert SAFETENSORS_DIR -o OUT.gguf [--outtype f16]\n"
" bankml version"
)
USAGE_034 = "usage: bankml usage [PID …]\n bankml serve FILE --fork FORK.json\n bankml version"
class FakeBankml:
"""Records argv; answers `version`, `--help`, `convert`, `create`, `sha256` like bankml."""
def __init__(self, usage: str = USAGE_035, create_rc: int = 0, create_err: str = "",
version: str = "0.3.5") -> None:
self.calls: list[list[str]] = []
self.version = version
self.usage = usage
self.create_rc = create_rc
self.create_err = create_err
self.modelfile_text = ""
def __call__(self, cmd: list[str], **_: Any) -> subprocess.CompletedProcess[str]:
self.calls.append(list(cmd))
verb = cmd[1]
if verb == "version":
return subprocess.CompletedProcess(cmd, 0, f"bankml {self.version}\n", "")
if verb == "--help":
return subprocess.CompletedProcess(cmd, 1, "", self.usage)
if verb == "convert":
out = cmd[cmd.index("-o") + 1]
return subprocess.CompletedProcess(cmd, 0, f"{SHA} {out}\n", "bankml convert: ok\n")
if verb == "create":
self.modelfile_text = Path(cmd[cmd.index("-f") + 1]).read_text()
if self.create_rc:
return subprocess.CompletedProcess(cmd, self.create_rc, "parsing modelfile\n",
self.create_err)
name = cmd[2]
return subprocess.CompletedProcess(
cmd, 0, f"parsing modelfile\npinned {name}-F16.gguf sha256:{SHA}\n",
f"bankml create: {name} (digest sha256:{DIGEST}) over {name}-F16.gguf "
f"(sha256 {SHA})\n")
if verb == "sha256":
return subprocess.CompletedProcess(cmd, 0, f"{SHA} {cmd[2]}\n", "")
raise AssertionError(cmd)
@pytest.fixture
def fake(monkeypatch: pytest.MonkeyPatch) -> FakeBankml:
f = FakeBankml()
monkeypatch.setattr(BP.shutil, "which", lambda name: "/usr/local/bin/bankml")
monkeypatch.setattr(BP.subprocess, "run", f)
return f
def _merged(tmp_path: Path, arch: str = "LlamaForCausalLM") -> Path:
d = tmp_path / "merged"
d.mkdir()
(d / "config.json").write_text(json.dumps({"architectures": [arch]}))
return d
# ---- sanitize: the refusal table ----------------------------------------------
@pytest.mark.parametrize(
("spec_kw", "needle"),
[
({"adapter": "./lora"}, "ADAPTER"),
({"template": "{{ .Prompt }}"}, "TEMPLATE"),
({"parameters": {"repeat_penalty": 1.3}}, "PARAMETER repeat_penalty: penalties"),
({"parameters": {"presence_penalty": 0.0}}, "PARAMETER presence_penalty"),
({"parameters": {"frequency_penalty": 0.5}}, "PARAMETER frequency_penalty"),
({"parameters": {"repeat_last_n": 64}}, "PARAMETER repeat_last_n"),
({"parameters": {"mirostat": 2}}, "mirostat is not reproduced"),
({"parameters": {"mirostat_tau": 5.0}}, "mirostat is not reproduced"),
({"parameters": {"typical_p": 0.9}}, "typical_p"),
({"parameters": {"num_thread": 4}}, "resource option"),
({"parameters": {"num_gpu": 0}}, "resource option"),
({"parameters": {"stop": "x"}}, "ModelfileSpec.stop"),
({"parameters": {"weird": 1}}, "not a parameter bankml reproduces"),
({"system": 'say """hi"""'}, "SYSTEM"),
],
)
def test_sanitize_refuses_and_names_it(spec_kw: dict[str, Any], needle: str) -> None:
res = BP.bankml_sanitize(ModelfileSpec(from_model="x", **spec_kw))
assert not res.ok
assert any(needle in r for r in res.refusals), res.refusals
def test_sanitize_accepts_the_subset_unchanged() -> None:
spec = ModelfileSpec(
from_model="/m", system="You are mindX.", license="Apache-2.0",
parameters={"temperature": 0.0, "top_k": 40, "top_p": 0.9, "min_p": 0.05, "seed": 42,
"num_ctx": 2048, "num_predict": 128},
stop=["<|im_end|>"],
messages=[{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}],
)
res = BP.bankml_sanitize(spec)
assert res.ok and res.spec == spec
text = render_modelfile(res.spec)
assert "PARAMETER seed 42" in text and 'PARAMETER stop "<|im_end|>"' in text
def test_template_passes_only_when_equal_to_base() -> None:
spec = ModelfileSpec(from_model="x", template="T")
assert BP.bankml_sanitize(spec, base_template="T").ok
assert not BP.bankml_sanitize(spec, base_template="U").ok
def test_arch_checks(tmp_path: Path) -> None:
assert BP.check_merged_arch(_merged(tmp_path)) is None
q = tmp_path / "q"
q.mkdir()
(q / "config.json").write_text(json.dumps({"architectures": ["Qwen3ForCausalLM"]}))
assert "Llama-architecture" in (BP.check_merged_arch(q) or "")
assert BP.base_family_refusal("HuggingFaceTB/SmolLM2-135M") is None
assert "qwen" in (BP.base_family_refusal("Qwen/Qwen3-1.7B") or "")
# ---- capability probe ----------------------------------------------------------
def test_missing_binary(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(BP.shutil, "which", lambda name: None)
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path))
assert res.status == "bankml_missing"
assert "github.com/cryptoAGI/bankml" in res.reason
def test_too_old_bankml_is_reported_not_raised(fake: FakeBankml, tmp_path: Path) -> None:
fake.usage = USAGE_034
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path))
assert res.status == "bankml_too_old"
assert "0.3.5" in res.reason and "create" in res.reason
assert not any(c[1] == "create" for c in fake.calls)
def test_capabilities_read_from_usage(fake: FakeBankml) -> None:
caps = BP.bankml_capabilities()
assert caps.version == "0.3.5" and caps.has_create and caps.has_convert
# ---- push ---------------------------------------------------------------------
def test_push_from_merged_dir(fake: FakeBankml, tmp_path: Path) -> None:
merged = _merged(tmp_path)
res = BP.push_to_bankml(
"HuggingFaceTB/SmolLM2-135M", "mindx-gen99", merged_dir=merged,
system="You are mindX.", params={"temperature": 0.0, "seed": 1}, stop=["<|im_end|>"],
registry_dir=tmp_path / "forks", work_dir=tmp_path / "work",
)
assert res.ok, res
assert res.model_sha256 == SHA and res.digest == DIGEST
create = next(c for c in fake.calls if c[1] == "create")
assert create[2] == "mindx-gen99" and "--registry" in create
# converted through a link named after the tag: the GGUF's general.name is "Mindx Gen99"
link = tmp_path / "work" / "mindx-gen99" / "mindx-gen99"
assert fake.modelfile_text.startswith(f"FROM {link}\n") and link.resolve() == merged.resolve()
assert 'SYSTEM """You are mindX."""' in fake.modelfile_text
assert "repeat_penalty" not in fake.modelfile_text
def test_push_with_convert_first(fake: FakeBankml, tmp_path: Path) -> None:
res = BP.push_to_bankml("b", "gen7", merged_dir=_merged(tmp_path), convert=True,
registry_dir=tmp_path / "forks", work_dir=tmp_path / "work")
assert res.ok
conv = next(c for c in fake.calls if c[1] == "convert")
assert conv[conv.index("-o") + 1].endswith("gen7-base-F16.gguf") and "--fork" in conv
assert fake.modelfile_text.startswith("FROM ") and "gen7-base-F16.gguf" in fake.modelfile_text
def test_refused_params_never_reach_bankml(fake: FakeBankml, tmp_path: Path) -> None:
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path),
params={"mirostat": 2, "temperature": 0.7})
assert res.status == "refused"
assert any("mirostat" in r for r in res.refusals)
assert fake.calls == [] # refused before even probing the binary
def test_penalties_refused_before_0_3_6(fake: FakeBankml, tmp_path: Path) -> None:
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path),
params={"repeat_penalty": 1.3, "temperature": 0.7})
assert res.status == "refused" and res.bankml_version == "0.3.5"
assert any("repeat_penalty" in r and "0.3.6" in r for r in res.refusals)
assert not any(c[1] in ("create", "convert") for c in fake.calls)
def test_penalties_taken_from_0_3_6(fake: FakeBankml, tmp_path: Path) -> None:
fake.version = "0.3.6"
res = BP.push_to_bankml("b", "mindx-gen99", merged_dir=_merged(tmp_path), work_dir=tmp_path / "w",
params={"repeat_penalty": 1.3, "num_ctx": 2048}, stop=["<|im_end|>"])
assert res.ok, res
assert "PARAMETER repeat_penalty 1.3" in fake.modelfile_text
def test_version_gate() -> None:
assert BP.penalties_supported("0.3.6") and BP.penalties_supported("0.4.0")
assert not BP.penalties_supported("0.3.5") and not BP.penalties_supported("")
assert BP.bankml_sanitize(ModelfileSpec(from_model="x", parameters={"repeat_penalty": 1.3})).refusals
assert BP.bankml_sanitize(ModelfileSpec(from_model="x", parameters={"repeat_penalty": 1.3}),
penalties=True).ok
def test_merged_dir_already_named_after_the_tag_is_used_as_is(fake: FakeBankml, tmp_path: Path) -> None:
merged = tmp_path / "mindx-gen99"
merged.mkdir()
(merged / "config.json").write_text(json.dumps({"architectures": ["LlamaForCausalLM"]}))
res = BP.push_to_bankml("b", "mindx-gen99", merged_dir=merged, work_dir=tmp_path / "w")
assert res.ok and fake.modelfile_text.startswith(f"FROM {merged.resolve()}\n")
def test_non_llama_merged_dir_refused(fake: FakeBankml, tmp_path: Path) -> None:
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path, "Qwen3ForCausalLM"))
assert res.status == "refused" and "Llama-architecture" in res.reason
assert not any(c[1] == "create" for c in fake.calls)
def test_create_refusal_is_a_result(fake: FakeBankml, tmp_path: Path) -> None:
fake.create_rc = 2
fake.create_err = "bankml create: refuse: FROM /x: tokenizer not seen\n"
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path), work_dir=tmp_path / "w")
assert res.status == "refused"
assert "tokenizer not seen" in res.reason
def test_bad_tag_and_bad_inputs(tmp_path: Path) -> None:
assert BP.push_to_bankml("b", "Bad:tag", merged_dir=tmp_path).status == "refused"
assert BP.push_to_bankml("b", "ok").status == "error"
def test_adapter_merge_and_import_error(fake: FakeBankml, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
from mindxtrain.deploy import ollama_push
def merged_stub(base: str, adapter: Path, out: Path, *, sink: Any = None) -> Path:
out.mkdir(parents=True)
(out / "config.json").write_text(json.dumps({"architectures": ["LlamaForCausalLM"]}))
return out
adapter = tmp_path / "checkpoint"
adapter.mkdir()
monkeypatch.setattr(ollama_push, "merge_lora_adapter", merged_stub)
res = BP.push_to_bankml("HuggingFaceTB/SmolLM2-135M", "g", adapter_dir=adapter,
work_dir=tmp_path / "w", registry_dir=tmp_path / "r")
assert res.ok and res.merged_dir == tmp_path / "w" / "merged"
def no_ml(*a: Any, **k: Any) -> Path:
raise ImportError("push-to-ollama needs `peft` + `transformers`")
monkeypatch.setattr(ollama_push, "merge_lora_adapter", no_ml)
res = BP.push_to_bankml("b", "g2", adapter_dir=adapter, work_dir=tmp_path / "w2")
assert res.status == "merge_failed" and "uv sync --extra ml" in res.reason
def test_register_with_mindx_uses_bankml_provider(fake: FakeBankml, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
from mindxtrain.deploy import api_client
seen: dict[str, Any] = {}
def swap(**kw: Any) -> dict[str, str]:
seen.update(kw)
return {"previous": "ollama:x", "current": "bankml:t"}
monkeypatch.setattr(api_client, "swap_mindx_fallback_model", swap)
res = BP.push_to_bankml("b", "t", merged_dir=_merged(tmp_path), register_with_mindx=True,
work_dir=tmp_path / "w", registry_dir=tmp_path / "r")
assert res.ok and seen["provider"] == "bankml" and seen["model"] == "t"
assert res.mindx_fallback_swap == {"previous": "ollama:x", "current": "bankml:t"}
# ---- CLI: serve --to bankml ------------------------------------------------------
def _recipe(tmp_path: Path, **overrides: Any) -> Path:
import yaml
from mindxtrain.config.loader import render_recipe
data = yaml.safe_load(render_recipe("mindx_fallback_qwen3_1_5b_cpu_real"))
for dotted, value in overrides.items():
node = data
*parents, leaf = dotted.split(".")
for p in parents:
node = node[p]
node[leaf] = value
path = tmp_path / "run.yaml"
path.write_text(yaml.safe_dump(data))
return path
def test_cli_refuses_quantized_config(tmp_path: Path) -> None:
from mindxtrain.cli.main import app
cfg = _recipe(tmp_path, **{"quantize.enabled": True, "quantize.scheme": "quark_fp8"})
r = CliRunner().invoke(app, ["serve", str(cfg), "--to", "bankml"])
assert r.exit_code == 2
assert "bankml refuses quantize.scheme=quark_fp8" in r.output
def test_cli_refuses_unconvertible_family(tmp_path: Path) -> None:
from mindxtrain.cli.main import app
cfg = _recipe(tmp_path, **{"model.name": "Qwen/Qwen3-1.7B"})
r = CliRunner().invoke(app, ["serve", str(cfg), "--to", "bankml"])
assert r.exit_code == 2
assert "qwen" in r.output.lower()
def test_cli_too_old_exits_2(tmp_path: Path, fake: FakeBankml) -> None:
from mindxtrain.cli.main import app
fake.usage = USAGE_034
ckpt = tmp_path / "ck"
ckpt.mkdir()
(ckpt / "config.json").write_text(json.dumps({"architectures": ["LlamaForCausalLM"]}))
cfg = _recipe(tmp_path)
r = CliRunner().invoke(app, ["serve", str(cfg), "--to", "bankml", "--checkpoint", str(ckpt)])
assert r.exit_code == 2
assert "bankml_too_old" in r.output