light-heart-labs--dreamserver
9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
331 行
13 KiB
Python
331 行
13 KiB
Python
"""Tests for config.py — manifest loading and service discovery."""
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
import config
|
|
from config import _detect_container_default_gateway, load_extension_manifests, _read_manifest_file
|
|
|
|
|
|
VALID_MANIFEST = """\
|
|
schema_version: ods.services.v1
|
|
service:
|
|
id: test-service
|
|
name: Test Service
|
|
port: 8080
|
|
health: /health
|
|
gpu_backends: [amd, nvidia]
|
|
external_port_default: 8080
|
|
features:
|
|
- id: test-feature
|
|
name: Test Feature
|
|
icon: Zap
|
|
category: inference
|
|
gpu_backends: [amd, nvidia]
|
|
"""
|
|
|
|
|
|
class TestReadManifestFile:
|
|
|
|
def test_reads_yaml(self, tmp_path):
|
|
f = tmp_path / "manifest.yaml"
|
|
f.write_text(VALID_MANIFEST)
|
|
data = _read_manifest_file(f)
|
|
assert data["schema_version"] == "ods.services.v1"
|
|
assert data["service"]["id"] == "test-service"
|
|
|
|
def test_reads_json(self, tmp_path):
|
|
import json
|
|
f = tmp_path / "manifest.json"
|
|
f.write_text(json.dumps({
|
|
"schema_version": "ods.services.v1",
|
|
"service": {"id": "json-svc", "name": "JSON", "port": 9090},
|
|
}))
|
|
data = _read_manifest_file(f)
|
|
assert data["service"]["id"] == "json-svc"
|
|
|
|
def test_rejects_non_dict_root(self, tmp_path):
|
|
f = tmp_path / "manifest.yaml"
|
|
f.write_text("- just\n- a\n- list\n")
|
|
with pytest.raises(ValueError, match="object"):
|
|
_read_manifest_file(f)
|
|
|
|
|
|
class TestHostAgentResolution:
|
|
|
|
def test_detect_container_default_gateway_reads_little_endian_route(self, tmp_path):
|
|
route = tmp_path / "route"
|
|
route.write_text(
|
|
"Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"
|
|
"eth0\t00000000\t010012AC\t0003\t0\t0\t0\t00000000\t0\t0\t0\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert _detect_container_default_gateway(str(route)) == "172.18.0.1"
|
|
|
|
def test_detect_container_default_gateway_requires_gateway_flag(self, tmp_path):
|
|
route = tmp_path / "route"
|
|
route.write_text(
|
|
"Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"
|
|
"eth0\t00000000\t010012AC\t0001\t0\t0\t0\t00000000\t0\t0\t0\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert _detect_container_default_gateway(str(route)) == ""
|
|
|
|
def test_resolve_agent_host_honors_explicit_env(self, monkeypatch):
|
|
monkeypatch.setenv("ODS_AGENT_HOST", " 10.9.8.7 ")
|
|
monkeypatch.setattr(config, "_running_inside_container", lambda: True)
|
|
monkeypatch.setattr(config, "_detect_container_default_gateway", lambda: "172.18.0.1")
|
|
|
|
assert config._resolve_agent_host() == "10.9.8.7"
|
|
|
|
def test_resolve_agent_host_uses_gateway_inside_container(self, monkeypatch):
|
|
monkeypatch.delenv("ODS_AGENT_HOST", raising=False)
|
|
monkeypatch.setattr(config, "_running_inside_container", lambda: True)
|
|
monkeypatch.setattr(config, "_detect_container_default_gateway", lambda: "172.18.0.1")
|
|
|
|
assert config._resolve_agent_host() == "172.18.0.1"
|
|
|
|
def test_resolve_agent_host_falls_back_outside_container(self, monkeypatch):
|
|
monkeypatch.delenv("ODS_AGENT_HOST", raising=False)
|
|
monkeypatch.setattr(config, "_running_inside_container", lambda: False)
|
|
monkeypatch.setattr(config, "_detect_container_default_gateway", lambda: "192.168.1.1")
|
|
|
|
assert config._resolve_agent_host() == "host.docker.internal"
|
|
|
|
|
|
class TestLoadExtensionManifests:
|
|
|
|
def test_loads_valid_manifest(self, tmp_path):
|
|
svc_dir = tmp_path / "test-service"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(VALID_MANIFEST)
|
|
|
|
services, features, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
assert "test-service" in services
|
|
assert services["test-service"]["port"] == 8080
|
|
assert services["test-service"]["name"] == "Test Service"
|
|
assert services["test-service"]["health"] == "/health"
|
|
assert len(features) == 1
|
|
assert features[0]["id"] == "test-feature"
|
|
|
|
def test_external_port_default_zero_disables_external_port_fallback(self, tmp_path):
|
|
svc_dir = tmp_path / "internal-service"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n"
|
|
" id: internal-service\n"
|
|
" name: Internal Service\n"
|
|
" port: 9119\n"
|
|
" external_port_default: 0\n"
|
|
" health: /api/status\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
|
|
assert services["internal-service"]["port"] == 9119
|
|
assert services["internal-service"]["external_port"] == 0
|
|
|
|
def test_preserves_host_network_flag(self, tmp_path):
|
|
svc_dir = tmp_path / "host-network-service"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n"
|
|
" id: host-network-service\n"
|
|
" name: Host Network Service\n"
|
|
" host_network: true\n"
|
|
" port: 0\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
|
|
assert services["host-network-service"]["host_network"] is True
|
|
|
|
def test_skips_wrong_schema_version(self, tmp_path):
|
|
svc_dir = tmp_path / "old-service"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v0\nservice:\n id: old\n port: 80\n"
|
|
)
|
|
|
|
services, features, errors = load_extension_manifests(tmp_path, "nvidia")
|
|
assert len(services) == 0
|
|
assert len(errors) == 1
|
|
assert "Unsupported schema_version" in errors[0]["error"]
|
|
|
|
def test_filters_by_gpu_backend(self, tmp_path):
|
|
svc_dir = tmp_path / "nvidia-only"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: nvidia-only\n name: NVIDIA Only\n port: 80\n"
|
|
" gpu_backends: [nvidia]\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "amd")
|
|
assert len(services) == 0
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
assert "nvidia-only" in services
|
|
|
|
def test_empty_directory(self, tmp_path):
|
|
services, features, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
assert services == {}
|
|
assert features == []
|
|
|
|
def test_nonexistent_directory(self, tmp_path):
|
|
missing = tmp_path / "does-not-exist"
|
|
services, features, _ = load_extension_manifests(missing, "nvidia")
|
|
assert services == {}
|
|
assert features == []
|
|
|
|
def test_features_filtered_by_gpu(self, tmp_path):
|
|
svc_dir = tmp_path / "mixed"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: mixed\n name: Mixed\n port: 80\n"
|
|
" gpu_backends: [amd, nvidia]\n"
|
|
"features:\n"
|
|
" - id: amd-feat\n name: AMD Feature\n gpu_backends: [amd]\n"
|
|
" - id: both-feat\n name: Both Feature\n gpu_backends: [amd, nvidia]\n"
|
|
)
|
|
|
|
_, features, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
feature_ids = [f["id"] for f in features]
|
|
assert "both-feat" in feature_ids
|
|
assert "amd-feat" not in feature_ids
|
|
|
|
def test_apple_backend_discovers_services_without_explicit_list(self, tmp_path):
|
|
"""Services with no gpu_backends key default to [amd, nvidia, apple]."""
|
|
svc_dir = tmp_path / "generic-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: generic-svc\n name: Generic\n port: 80\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "apple")
|
|
assert "generic-svc" in services
|
|
|
|
def test_apple_backend_filtered_by_explicit_nvidia_amd_list(self, tmp_path):
|
|
"""Docker service explicitly listing [amd, nvidia] is still loaded for apple backend."""
|
|
svc_dir = tmp_path / "gpu-only-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: gpu-only-svc\n name: GPU Only\n port: 80\n"
|
|
" gpu_backends: [amd, nvidia]\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "apple")
|
|
assert "gpu-only-svc" in services
|
|
|
|
def test_apple_backend_discovers_service_explicitly_listing_apple(self, tmp_path):
|
|
"""Service that lists apple in gpu_backends is discovered for apple backend."""
|
|
svc_dir = tmp_path / "apple-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: apple-svc\n name: Apple Svc\n port: 80\n"
|
|
" gpu_backends: [amd, nvidia, apple]\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "apple")
|
|
assert "apple-svc" in services
|
|
|
|
def test_apple_backend_feature_default_discovered(self, tmp_path):
|
|
"""Features with no gpu_backends key default to include apple."""
|
|
svc_dir = tmp_path / "svc-with-feature"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: svc\n name: Svc\n port: 80\n"
|
|
"features:\n"
|
|
" - id: default-feat\n name: Default Feature\n"
|
|
)
|
|
|
|
_, features, _ = load_extension_manifests(tmp_path, "apple")
|
|
assert any(f["id"] == "default-feat" for f in features)
|
|
|
|
def test_apple_backend_excludes_host_systemd(self, tmp_path):
|
|
"""Services with type: host-systemd are excluded on apple backend."""
|
|
svc_dir = tmp_path / "systemd-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: systemd-svc\n name: Systemd Svc\n port: 80\n"
|
|
" type: host-systemd\n"
|
|
" gpu_backends: [amd, nvidia]\n"
|
|
)
|
|
|
|
services, _, _ = load_extension_manifests(tmp_path, "apple")
|
|
assert "systemd-svc" not in services
|
|
|
|
def test_apple_backend_loads_all_features(self, tmp_path):
|
|
"""Features with gpu_backends: [amd, nvidia] are loaded for apple backend."""
|
|
svc_dir = tmp_path / "svc-with-gpu-feature"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: svc\n name: Svc\n port: 80\n"
|
|
"features:\n"
|
|
" - id: gpu-feat\n name: GPU Feature\n gpu_backends: [amd, nvidia]\n"
|
|
)
|
|
|
|
_, features, _ = load_extension_manifests(tmp_path, "apple")
|
|
assert any(f["id"] == "gpu-feat" for f in features)
|
|
|
|
def test_warns_on_missing_optional_feature_fields(self, tmp_path, caplog):
|
|
"""A feature missing optional fields is loaded but a warning is logged."""
|
|
svc_dir = tmp_path / "sparse-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n id: sparse-svc\n name: Sparse\n port: 80\n"
|
|
"features:\n"
|
|
" - id: sparse-feat\n name: Sparse Feature\n"
|
|
)
|
|
|
|
with caplog.at_level(logging.WARNING, logger="config"):
|
|
_, features, _ = load_extension_manifests(tmp_path, "nvidia")
|
|
|
|
assert any(f["id"] == "sparse-feat" for f in features)
|
|
warning_msgs = [r.message for r in caplog.records if "missing optional fields" in r.message]
|
|
assert len(warning_msgs) == 1
|
|
assert "sparse-feat" in warning_msgs[0]
|
|
for field in ("description", "icon", "category", "setup_time", "priority"):
|
|
assert field in warning_msgs[0]
|
|
|
|
def test_collects_parse_errors(self, tmp_path):
|
|
svc_dir = tmp_path / "broken-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text("invalid: yaml: [unterminated")
|
|
|
|
services, features, errors = load_extension_manifests(tmp_path, "nvidia")
|
|
assert len(errors) == 1
|
|
assert "file" in errors[0]
|
|
assert "error" in errors[0]
|
|
assert "broken-svc" in errors[0]["file"]
|
|
assert services == {}
|
|
assert features == []
|
|
|
|
def test_collects_error_for_missing_service_id(self, tmp_path):
|
|
"""A manifest with a valid schema but no service.id collects an error."""
|
|
svc_dir = tmp_path / "no-id-svc"
|
|
svc_dir.mkdir()
|
|
(svc_dir / "manifest.yaml").write_text(
|
|
"schema_version: ods.services.v1\n"
|
|
"service:\n name: No ID\n port: 80\n"
|
|
)
|
|
|
|
services, features, errors = load_extension_manifests(tmp_path, "nvidia")
|
|
assert len(errors) == 1
|
|
assert "service.id is required" in errors[0]["error"]
|
|
assert "no-id-svc" in errors[0]["file"]
|
|
assert services == {}
|