226 lines
6.7 KiB
Python
226 lines
6.7 KiB
Python
"""Tests for workflow-declared Vast.ai marketplace requirements."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
|
|
def test_profile_builds_documented_hard_filter_payload(
|
|
vast_models_module: Any,
|
|
) -> None:
|
|
"""Search requests should express every user spending and capacity guard."""
|
|
profile = vast_models_module.VastResourceProfile(
|
|
profile_id="17",
|
|
profile_name="large-video",
|
|
gpu_count=2,
|
|
minimum_gpu_ram_mb=48 * 1024,
|
|
minimum_total_flops=150.0,
|
|
minimum_cpu_ram_mb=128 * 1024,
|
|
minimum_cpu_cores=24.0,
|
|
allocated_disk_gb=500.0,
|
|
maximum_hourly_cost_usd=2.5,
|
|
allowed_geolocations=("US", "CA"),
|
|
)
|
|
|
|
payload = profile.search_payload(limit=12)
|
|
|
|
assert payload["num_gpus"] == {"eq": 2}
|
|
assert payload["gpu_ram"] == {"gte": 48 * 1024}
|
|
assert payload["total_flops"] == {"gte": 150.0}
|
|
assert payload["cpu_ram"] == {"gte": 128 * 1024}
|
|
assert payload["allocated_storage"] == 500.0
|
|
assert payload["dph_total"] == {"lte": 2.5}
|
|
assert payload["geolocation"] == {"in": ["US", "CA"]}
|
|
assert payload["order"] == [["dph_total", "asc"]]
|
|
assert profile.environment_id == "vast:17"
|
|
|
|
|
|
def test_any_profile_omits_every_optional_marketplace_filter(
|
|
vast_models_module: Any,
|
|
) -> None:
|
|
"""None-backed Any selectors should produce the broadest safe search."""
|
|
profile = vast_models_module.VastResourceProfile(
|
|
profile_id="any",
|
|
profile_name="any",
|
|
)
|
|
|
|
payload = profile.search_payload()
|
|
|
|
assert set(payload) == {
|
|
"limit",
|
|
"type",
|
|
"rentable",
|
|
"rented",
|
|
"gpu_arch",
|
|
"cpu_arch",
|
|
"gpu_frac",
|
|
"gpu_display_active",
|
|
"disk_space",
|
|
"allocated_storage",
|
|
"cuda_max_good",
|
|
"direct_port_count",
|
|
"order",
|
|
}
|
|
|
|
|
|
def test_offer_ranking_is_price_first_and_deterministic(
|
|
vast_models_module: Any,
|
|
) -> None:
|
|
"""The cheapest compatible offer should win before quality tie-breakers."""
|
|
profile = vast_models_module.VastResourceProfile(
|
|
profile_id="one",
|
|
profile_name="one",
|
|
)
|
|
base = {
|
|
"gpu_name": "GPU",
|
|
"num_gpus": 1,
|
|
"gpu_ram": 24 * 1024,
|
|
"gpu_total_ram": 24 * 1024,
|
|
"total_flops": 100,
|
|
"cpu_ram": 64 * 1024,
|
|
"cpu_cores_effective": 16,
|
|
"disk_space": 500,
|
|
"duration": 30 * 86400,
|
|
"reliability": 0.999,
|
|
"dlperf": 50,
|
|
"inet_down": 1000,
|
|
"cuda_max_good": 13.0,
|
|
"direct_port_count": 1,
|
|
"verification": "verified",
|
|
}
|
|
expensive = vast_models_module.VastOffer.from_api(
|
|
{**base, "id": 2, "dph_total": 0.8}
|
|
)
|
|
cheap = vast_models_module.VastOffer.from_api(
|
|
{**base, "id": 1, "dph_total": 0.4, "reliability": 0.991}
|
|
)
|
|
|
|
assert vast_models_module.compatible_offers((expensive, cheap), profile) == (
|
|
cheap,
|
|
expensive,
|
|
)
|
|
|
|
|
|
def test_offer_is_revalidated_after_server_filtering(vast_models_module: Any) -> None:
|
|
"""A malformed server result must not bypass client-side constraints."""
|
|
profile = vast_models_module.VastResourceProfile(
|
|
profile_id="large",
|
|
profile_name="large",
|
|
minimum_gpu_ram_mb=80 * 1024,
|
|
)
|
|
offer = vast_models_module.VastOffer.from_api(
|
|
{
|
|
"id": 8,
|
|
"gpu_name": "Small GPU",
|
|
"num_gpus": 1,
|
|
"gpu_ram": 24 * 1024,
|
|
"gpu_total_ram": 24 * 1024,
|
|
"total_flops": 100,
|
|
"cpu_ram": 64 * 1024,
|
|
"cpu_cores_effective": 16,
|
|
"disk_space": 500,
|
|
"duration": 30 * 86400,
|
|
"reliability": 0.999,
|
|
"dlperf": 50,
|
|
"inet_down": 1000,
|
|
"cuda_max_good": 13.0,
|
|
"direct_port_count": 1,
|
|
"dph_total": 0.2,
|
|
"verification": "verified",
|
|
}
|
|
)
|
|
|
|
assert offer.incompatibility_reason(profile) == "insufficient GPU RAM"
|
|
assert vast_models_module.compatible_offers((offer,), profile) == ()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value"),
|
|
[
|
|
("profile_name", "unsafe profile"),
|
|
("maximum_hourly_cost_usd", 0.0),
|
|
("minimum_reliability", 1.1),
|
|
("maximum_instances", 0),
|
|
],
|
|
)
|
|
def test_profile_rejects_invalid_or_unsafe_values(
|
|
vast_models_module: Any,
|
|
field: str,
|
|
value: Any,
|
|
) -> None:
|
|
"""Invalid workflow widgets should fail before marketplace access."""
|
|
arguments = {
|
|
"profile_id": "profile",
|
|
"profile_name": "profile",
|
|
field: value,
|
|
}
|
|
with pytest.raises(ValueError):
|
|
vast_models_module.VastResourceProfile(**arguments)
|
|
|
|
|
|
def test_launch_payload_uses_direct_ssh_and_fail_fast(
|
|
vast_models_module: Any,
|
|
) -> None:
|
|
"""Managed instances should expose SSH without queuing unavailable offers."""
|
|
spec = vast_models_module.VastInstanceLaunchSpec(
|
|
image="ghcr.io/example/worker@sha256:abc",
|
|
disk_gb=200,
|
|
label="comfy-modal:owner:profile",
|
|
onstart="/opt/comfy/bin/start",
|
|
environment={"COMFY_MODAL_REMOTE_WORKER": "1"},
|
|
)
|
|
|
|
payload = spec.to_api_payload()
|
|
|
|
assert payload["runtype"] == "ssh_direct"
|
|
assert payload["cancel_unavail"] is True
|
|
assert payload["target_state"] == "running"
|
|
assert "price" not in payload
|
|
|
|
|
|
def test_instance_normalizes_provider_progress_and_terminal_state(
|
|
vast_models_module: Any,
|
|
) -> None:
|
|
"""Vast's stopped image-pull state should be bounded, readable, and terminal."""
|
|
instance = vast_models_module.VastInstance.from_api(
|
|
{
|
|
"id": 42,
|
|
"actual_status": "loading",
|
|
"intended_status": "stopped",
|
|
"cur_state": "stopped",
|
|
"status_msg": " unauthorized:\n authentication required " + "x" * 300,
|
|
"num_gpus": 1,
|
|
"gpu_ram": 48 * 1024,
|
|
"cpu_ram": 96 * 1024,
|
|
}
|
|
)
|
|
|
|
assert instance.current_state == "stopped"
|
|
assert instance.status_message is not None
|
|
assert "\n" not in instance.status_message
|
|
assert len(instance.status_message) == 240
|
|
assert instance.terminal_failure is True
|
|
|
|
|
|
def test_instance_falls_back_from_unknown_actual_status(
|
|
vast_models_module: Any,
|
|
) -> None:
|
|
"""Provider progress should use current state when actual status is absent."""
|
|
instance = vast_models_module.VastInstance.from_api(
|
|
{
|
|
"id": 43,
|
|
"actual_status": None,
|
|
"intended_status": "running",
|
|
"cur_state": "loading",
|
|
"status_msg": "allocating host capacity",
|
|
"num_gpus": 1,
|
|
"gpu_ram": 96 * 1024,
|
|
"cpu_ram": 128 * 1024,
|
|
}
|
|
)
|
|
|
|
assert instance.actual_status == "unknown"
|
|
assert instance.lifecycle_status == "loading"
|