"""Tests for soup push command.""" import json from pathlib import Path from typer.testing import CliRunner from soup_cli.cli import app from soup_cli.commands.push import _format_size, _generate_model_card runner = CliRunner() # --- _format_size --- def test_format_size_bytes(): assert _format_size(512) == "512.0 B" def test_format_size_kb(): assert _format_size(2048) == "2.0 KB" def test_format_size_mb(): assert _format_size(5 * 1024 * 1024) == "5.0 MB" def test_format_size_gb(): assert _format_size(3 * 1024**3) == "3.0 GB" def test_format_size_tb(): assert _format_size(2 * 1024**4) == "2.0 TB" # --- _generate_model_card --- def test_model_card_adapter(tmp_path: Path): """Model card for LoRA adapter should include base model info.""" adapter_dir = tmp_path / "adapter" adapter_dir.mkdir() config = adapter_dir / "adapter_config.json" config.write_text(json.dumps({ "base_model_name_or_path": "meta-llama/Llama-3.1-8B", "r": 64, "lora_alpha": 16, })) card = _generate_model_card(adapter_dir, "user/my-model", is_adapter=True) assert "my-model" in card assert "meta-llama/Llama-3.1-8B" in card assert "LoRA rank" in card assert "soup-cli" in card def test_model_card_full_model(tmp_path: Path): """Model card for full model should not include LoRA info.""" model_dir = tmp_path / "model" model_dir.mkdir() card = _generate_model_card(model_dir, "user/full-model", is_adapter=False) assert "full-model" in card assert "fine-tuned language model" in card assert "soup-cli" in card def test_model_card_adapter_bad_config(tmp_path: Path): """Model card should handle malformed adapter_config.json gracefully.""" adapter_dir = tmp_path / "adapter" adapter_dir.mkdir() config = adapter_dir / "adapter_config.json" config.write_text("not valid json") card = _generate_model_card(adapter_dir, "user/broken", is_adapter=True) assert "broken" in card def test_model_card_repo_without_slash(): """Model card should handle repo ID without slash.""" import tempfile with tempfile.TemporaryDirectory() as td: card = _generate_model_card(Path(td), "my-model", is_adapter=False) assert "my-model" in card # --- CLI validation --- def test_push_missing_model(): result = runner.invoke(app, ["push", "--model", "/nonexistent", "--repo", "user/model"]) assert result.exit_code == 1 def test_push_not_a_directory(tmp_path: Path): fake_file = tmp_path / "model.bin" fake_file.write_text("content") result = runner.invoke( app, ["push", "--model", str(fake_file), "--repo", "user/model"] ) assert result.exit_code == 1 assert "directory" in result.output.lower() def test_push_invalid_model_dir(tmp_path: Path): """Empty directory (no adapter_config.json or config.json) should fail.""" model_dir = tmp_path / "empty" model_dir.mkdir() result = runner.invoke( app, ["push", "--model", str(model_dir), "--repo", "user/model"] ) assert result.exit_code == 1 def test_push_no_token(tmp_path: Path, monkeypatch): """Should fail if no HF token is available.""" monkeypatch.delenv("HF_TOKEN", raising=False) monkeypatch.delenv("HUGGINGFACE_HUB_TOKEN", raising=False) # Point HOME at an empty dir so the cached-token fallback also misses. monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path / "no-creds") # `soup push` now requires --model under cwd. monkeypatch.chdir(tmp_path) model_dir = tmp_path / "model" model_dir.mkdir() (model_dir / "adapter_config.json").write_text("{}") (model_dir / "adapter_model.safetensors").write_text("fake") result = runner.invoke( app, ["push", "--model", str(model_dir), "--repo", "user/model"] ) assert result.exit_code == 1 assert "token" in result.output.lower()