This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/tests/test_infra/test_vast_client.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

548 lines
17 KiB
Python

"""
Tests fuer VastAIClient.
Testet den vast.ai REST API Client.
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from datetime import datetime, timezone
from infra.vast_client import (
VastAIClient,
InstanceInfo,
InstanceStatus,
AccountInfo,
)
class TestInstanceStatus:
"""Tests fuer InstanceStatus Enum."""
def test_status_values(self):
"""Test alle Status-Werte."""
assert InstanceStatus.RUNNING.value == "running"
assert InstanceStatus.STOPPED.value == "stopped"
assert InstanceStatus.EXITED.value == "exited"
assert InstanceStatus.LOADING.value == "loading"
assert InstanceStatus.SCHEDULING.value == "scheduling"
assert InstanceStatus.CREATING.value == "creating"
assert InstanceStatus.UNKNOWN.value == "unknown"
class TestInstanceInfo:
"""Tests fuer InstanceInfo Dataclass."""
def test_create_basic(self):
"""Test grundlegende Erstellung."""
info = InstanceInfo(
id=12345,
status=InstanceStatus.RUNNING,
)
assert info.id == 12345
assert info.status == InstanceStatus.RUNNING
assert info.gpu_name is None
assert info.num_gpus == 1
def test_from_api_response_running(self):
"""Test Parse von API Response (running)."""
api_data = {
"id": 67890,
"actual_status": "running",
"machine_id": 111,
"gpu_name": "RTX 3090",
"num_gpus": 1,
"gpu_ram": 24.0,
"cpu_ram": 64.0,
"disk_space": 200.0,
"dph_total": 0.45,
"public_ipaddr": "192.168.1.100",
"ports": {
"8001/tcp": [{"HostIp": "0.0.0.0", "HostPort": "12345"}],
},
"label": "llm-server",
"start_date": 1702900800, # Unix timestamp
}
info = InstanceInfo.from_api_response(api_data)
assert info.id == 67890
assert info.status == InstanceStatus.RUNNING
assert info.gpu_name == "RTX 3090"
assert info.dph_total == 0.45
assert info.public_ipaddr == "192.168.1.100"
assert info.label == "llm-server"
def test_from_api_response_stopped(self):
"""Test Parse von API Response (gestoppt)."""
api_data = {
"id": 11111,
"actual_status": "exited",
}
info = InstanceInfo.from_api_response(api_data)
assert info.status == InstanceStatus.EXITED
def test_from_api_response_unknown_status(self):
"""Test Parse von unbekanntem Status."""
api_data = {
"id": 22222,
"actual_status": "weird_status",
}
info = InstanceInfo.from_api_response(api_data)
assert info.status == InstanceStatus.UNKNOWN
def test_get_endpoint_url_with_port_mapping(self):
"""Test Endpoint URL mit Port-Mapping."""
info = InstanceInfo(
id=12345,
status=InstanceStatus.RUNNING,
public_ipaddr="10.0.0.1",
ports={
"8001/tcp": [{"HostIp": "0.0.0.0", "HostPort": "54321"}],
},
)
url = info.get_endpoint_url(8001)
assert url == "http://10.0.0.1:54321"
def test_get_endpoint_url_fallback(self):
"""Test Endpoint URL Fallback ohne Port-Mapping."""
info = InstanceInfo(
id=12345,
status=InstanceStatus.RUNNING,
public_ipaddr="10.0.0.2",
ports={},
)
url = info.get_endpoint_url(8001)
assert url == "http://10.0.0.2:8001"
def test_get_endpoint_url_no_ip(self):
"""Test Endpoint URL ohne IP."""
info = InstanceInfo(
id=12345,
status=InstanceStatus.RUNNING,
public_ipaddr=None,
)
url = info.get_endpoint_url(8001)
assert url is None
def test_to_dict(self):
"""Test Serialisierung."""
info = InstanceInfo(
id=12345,
status=InstanceStatus.RUNNING,
gpu_name="RTX 4090",
dph_total=0.75,
)
data = info.to_dict()
assert data["id"] == 12345
assert data["status"] == "running"
assert data["gpu_name"] == "RTX 4090"
assert data["dph_total"] == 0.75
class TestAccountInfo:
"""Tests fuer AccountInfo Dataclass."""
def test_create_basic(self):
"""Test grundlegende Erstellung."""
info = AccountInfo(
credit=25.50,
balance=0.0,
total_spend=10.25,
username="testuser",
email="test@example.com",
has_billing=True,
)
assert info.credit == 25.50
assert info.balance == 0.0
assert info.total_spend == 10.25
assert info.username == "testuser"
assert info.email == "test@example.com"
assert info.has_billing is True
def test_from_api_response_complete(self):
"""Test Parse von vollstaendiger API Response."""
api_data = {
"credit": 23.87674017153,
"balance": 0.0,
"total_spend": -1.1732598284700013, # API gibt negativ zurueck
"username": "benjamin",
"email": "benjamin@example.com",
"has_billing": True,
}
info = AccountInfo.from_api_response(api_data)
assert info.credit == 23.87674017153
assert info.balance == 0.0
assert info.total_spend == 1.1732598284700013 # Sollte positiv sein (abs)
assert info.username == "benjamin"
assert info.email == "benjamin@example.com"
assert info.has_billing is True
def test_from_api_response_minimal(self):
"""Test Parse von minimaler API Response."""
api_data = {}
info = AccountInfo.from_api_response(api_data)
assert info.credit == 0.0
assert info.balance == 0.0
assert info.total_spend == 0.0
assert info.username == ""
assert info.email == ""
assert info.has_billing is False
def test_from_api_response_partial(self):
"""Test Parse von teilweiser API Response."""
api_data = {
"credit": 50.0,
"username": "partial_user",
}
info = AccountInfo.from_api_response(api_data)
assert info.credit == 50.0
assert info.username == "partial_user"
assert info.email == ""
assert info.total_spend == 0.0
def test_to_dict(self):
"""Test Serialisierung zu Dictionary."""
info = AccountInfo(
credit=100.0,
balance=5.0,
total_spend=25.0,
username="dictuser",
email="dict@test.com",
has_billing=True,
)
data = info.to_dict()
assert data["credit"] == 100.0
assert data["balance"] == 5.0
assert data["total_spend"] == 25.0
assert data["username"] == "dictuser"
assert data["email"] == "dict@test.com"
assert data["has_billing"] is True
def test_total_spend_negative_to_positive(self):
"""Test dass negative total_spend Werte positiv werden."""
api_data = {
"total_spend": -99.99,
}
info = AccountInfo.from_api_response(api_data)
assert info.total_spend == 99.99
class TestVastAIClient:
"""Tests fuer VastAIClient."""
def test_init(self):
"""Test Client Initialisierung."""
client = VastAIClient(api_key="test-key", timeout=60.0)
assert client.api_key == "test-key"
assert client.timeout == 60.0
assert client._client is None
def test_build_url(self):
"""Test URL Building."""
client = VastAIClient(api_key="my-api-key")
url = client._build_url("/instances/")
assert url == "https://console.vast.ai/api/v0/instances/?api_key=my-api-key"
url2 = client._build_url("/instances/?foo=bar")
assert url2 == "https://console.vast.ai/api/v0/instances/?foo=bar&api_key=my-api-key"
@pytest.mark.asyncio
async def test_list_instances(self):
"""Test Liste aller Instanzen."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"instances": [
{"id": 1, "actual_status": "running", "gpu_name": "RTX 3090"},
{"id": 2, "actual_status": "exited"}, # exited statt stopped
]
}
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.return_value = mock_response
mock_get.return_value = mock_client
instances = await client.list_instances()
assert len(instances) == 2
assert instances[0].id == 1
assert instances[0].status == InstanceStatus.RUNNING
assert instances[1].id == 2
assert instances[1].status == InstanceStatus.EXITED
@pytest.mark.asyncio
async def test_get_instance(self):
"""Test einzelne Instanz abrufen."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"instances": [
{"id": 12345, "actual_status": "running", "dph_total": 0.5}
]
}
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.return_value = mock_response
mock_get.return_value = mock_client
instance = await client.get_instance(12345)
assert instance is not None
assert instance.id == 12345
assert instance.status == InstanceStatus.RUNNING
assert instance.dph_total == 0.5
@pytest.mark.asyncio
async def test_get_instance_not_found(self):
"""Test Instanz nicht gefunden."""
import httpx
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 404
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.side_effect = httpx.HTTPStatusError(
"Not Found", request=MagicMock(), response=mock_response
)
mock_get.return_value = mock_client
instance = await client.get_instance(99999)
assert instance is None
@pytest.mark.asyncio
async def test_start_instance(self):
"""Test Instanz starten."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.put.return_value = mock_response
mock_get.return_value = mock_client
success = await client.start_instance(12345)
assert success is True
mock_client.put.assert_called_once()
call_args = mock_client.put.call_args
assert call_args[1]["json"] == {"state": "running"}
@pytest.mark.asyncio
async def test_stop_instance(self):
"""Test Instanz stoppen."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.put.return_value = mock_response
mock_get.return_value = mock_client
success = await client.stop_instance(12345)
assert success is True
call_args = mock_client.put.call_args
assert call_args[1]["json"] == {"state": "stopped"}
@pytest.mark.asyncio
async def test_stop_instance_failure(self):
"""Test Instanz stoppen fehlschlaegt."""
import httpx
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 500
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.put.side_effect = httpx.HTTPStatusError(
"Error", request=MagicMock(), response=mock_response
)
mock_get.return_value = mock_client
success = await client.stop_instance(12345)
assert success is False
@pytest.mark.asyncio
async def test_destroy_instance(self):
"""Test Instanz loeschen."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.delete.return_value = mock_response
mock_get.return_value = mock_client
success = await client.destroy_instance(12345)
assert success is True
mock_client.delete.assert_called_once()
@pytest.mark.asyncio
async def test_set_label(self):
"""Test Label setzen."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.put.return_value = mock_response
mock_get.return_value = mock_client
success = await client.set_label(12345, "my-label")
assert success is True
call_args = mock_client.put.call_args
assert call_args[1]["json"] == {"label": "my-label"}
@pytest.mark.asyncio
async def test_close(self):
"""Test Client schliessen."""
client = VastAIClient(api_key="test-key")
# Erstelle Client
await client._get_client()
assert client._client is not None
# Schliesse
await client.close()
assert client._client is None
@pytest.mark.asyncio
async def test_get_account_info_success(self):
"""Test Account-Info erfolgreich abrufen."""
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"credit": 23.87674017153,
"balance": 0.0,
"total_spend": -1.1732598284700013,
"username": "testuser",
"email": "test@vast.ai",
"has_billing": True,
}
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.return_value = mock_response
mock_get.return_value = mock_client
account = await client.get_account_info()
assert account is not None
assert account.credit == 23.87674017153
assert account.total_spend == 1.1732598284700013 # abs()
assert account.username == "testuser"
assert account.email == "test@vast.ai"
assert account.has_billing is True
@pytest.mark.asyncio
async def test_get_account_info_api_error(self):
"""Test Account-Info bei API-Fehler."""
import httpx
client = VastAIClient(api_key="test-key")
mock_response = MagicMock()
mock_response.status_code = 401
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.side_effect = httpx.HTTPStatusError(
"Unauthorized", request=MagicMock(), response=mock_response
)
mock_get.return_value = mock_client
account = await client.get_account_info()
assert account is None
@pytest.mark.asyncio
async def test_get_account_info_network_error(self):
"""Test Account-Info bei Netzwerk-Fehler."""
client = VastAIClient(api_key="test-key")
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.side_effect = Exception("Network error")
mock_get.return_value = mock_client
account = await client.get_account_info()
assert account is None
@pytest.mark.asyncio
async def test_get_account_info_url(self):
"""Test dass get_account_info den korrekten Endpoint aufruft."""
client = VastAIClient(api_key="my-test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"credit": 10.0}
mock_response.raise_for_status = MagicMock()
with patch.object(client, "_get_client") as mock_get:
mock_client = AsyncMock()
mock_client.get.return_value = mock_response
mock_get.return_value = mock_client
await client.get_account_info()
# Pruefe dass /users/current/ aufgerufen wurde
call_args = mock_client.get.call_args
called_url = call_args[0][0]
assert "/users/current/" in called_url
assert "api_key=my-test-key" in called_url