feat(simulation): add portable gaussian provider connector

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 00:24:53 +03:00
parent 3ff3b9b587
commit 73fbfddf6c
4 changed files with 749 additions and 0 deletions
@@ -0,0 +1,414 @@
"""Portable connector for the external DC Gaussian Pipeline service."""
from __future__ import annotations
import base64
import hashlib
import json
import os
import re
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Final
from urllib.parse import quote, unquote, urljoin, urlparse
import httpx
TUS_VERSION: Final = "1.0.0"
BUILD_REQUEST_SCHEMA: Final = "gaussian-pipeline.build-request/v1"
JOB_SCHEMA: Final = "gaussian-pipeline.job/v1"
RESULT_SCHEMA: Final = "gaussian-pipeline.build-result/v1"
CAPABILITIES_SCHEMA: Final = "gaussian-pipeline.capabilities/v1"
SAFE_UPLOAD_ID: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
SHA256_PATTERN: Final = re.compile(r"^[a-f0-9]{64}$")
DEFAULT_CHUNK_BYTES: Final = 8 * 1024 * 1024
MAX_JSON_RESPONSE_BYTES: Final = 32 * 1024 * 1024
MAX_RETRIES: Final = 3
class GaussianPipelineGatewayError(RuntimeError):
"""The configured Gaussian provider violated or rejected its connector contract."""
class GaussianPipelineConfigurationError(GaussianPipelineGatewayError):
"""The provider connector configuration is incomplete or unsafe."""
class GaussianPipelineUnavailableError(GaussianPipelineGatewayError):
"""The provider cannot currently be reached."""
class GaussianPipelineIntegrityError(GaussianPipelineGatewayError):
"""Transferred bytes do not match their immutable descriptor."""
@dataclass(frozen=True, slots=True)
class GaussianSourceUpload:
upload_id: str
filename: str
format: str
sha256: str
byte_length: int
def to_dict(self) -> dict[str, object]:
return {
"upload_id": self.upload_id,
"filename": self.filename,
"format": self.format,
"sha256": self.sha256,
"byte_length": self.byte_length,
}
class GaussianPipelineGateway:
"""TUS and JSON client with bounded responses and independent digest checks."""
def __init__(
self,
endpoint: str,
token_file: Path,
*,
timeout_seconds: float = 30.0,
chunk_bytes: int = DEFAULT_CHUNK_BYTES,
transport: httpx.BaseTransport | None = None,
) -> None:
self.endpoint = _endpoint(endpoint)
self.token_file = token_file.expanduser().absolute()
self.token = _read_token(self.token_file)
if timeout_seconds <= 0:
raise GaussianPipelineConfigurationError("provider timeout must be positive")
if chunk_bytes <= 0 or chunk_bytes > 64 * 1024 * 1024:
raise GaussianPipelineConfigurationError("provider upload chunk size is invalid")
self.chunk_bytes = chunk_bytes
self._client = httpx.Client(
base_url=self.endpoint,
headers={"Authorization": f"Bearer {self.token}"},
timeout=httpx.Timeout(timeout_seconds),
transport=transport,
follow_redirects=False,
)
def close(self) -> None:
self._client.close()
def __enter__(self) -> GaussianPipelineGateway:
return self
def __exit__(self, *_: object) -> None:
self.close()
def capabilities(self) -> dict[str, Any]:
document = self._json("GET", "/v1/capabilities")
if (
document.get("schema_version") != CAPABILITIES_SCHEMA
or document.get("service") != "ndc-gaussian-pipeline"
):
raise GaussianPipelineGatewayError("Gaussian provider capabilities do not match v1")
return document
def upload_source(
self,
source_path: Path,
*,
filename: str,
source_format: str,
sha256: str,
) -> GaussianSourceUpload:
source_candidate = source_path.expanduser().absolute()
if source_candidate.is_symlink() or not source_candidate.is_file():
raise GaussianPipelineIntegrityError("Gaussian source must be one regular file")
source = source_candidate.resolve()
if source_format not in {"lcc", "lcc2"}:
raise GaussianPipelineIntegrityError("Gaussian source format must be lcc or lcc2")
if Path(filename).name != filename or not filename.lower().endswith(f".{source_format}"):
raise GaussianPipelineIntegrityError("Gaussian source filename is unsafe")
if SHA256_PATTERN.fullmatch(sha256) is None:
raise GaussianPipelineIntegrityError("Gaussian source digest is invalid")
byte_length = source.stat().st_size
if byte_length <= 0:
raise GaussianPipelineIntegrityError("Gaussian source is empty")
if _sha256(source) != sha256:
raise GaussianPipelineIntegrityError("Gaussian source digest does not match")
metadata = _tus_metadata(
{"filename": filename, "format": source_format, "sha256": sha256}
)
try:
response = self._client.post(
"/v1/uploads",
headers={
"Tus-Resumable": TUS_VERSION,
"Upload-Length": str(byte_length),
"Upload-Metadata": metadata,
},
content=b"",
)
response.raise_for_status()
except httpx.HTTPError as exc:
raise _unavailable("Gaussian upload could not be created", exc) from exc
location = response.headers.get("Location")
if location is None:
raise GaussianPipelineGatewayError("Gaussian upload response omitted Location")
upload_url = urljoin(str(response.request.url), location)
if not _is_provider_upload_url(self.endpoint, upload_url):
raise GaussianPipelineGatewayError("Gaussian upload Location escaped the provider")
upload_id = unquote(urlparse(upload_url).path.rsplit("/", 1)[-1])
if SAFE_UPLOAD_ID.fullmatch(upload_id) is None:
raise GaussianPipelineGatewayError("Gaussian upload id is invalid")
self._send_file(source, upload_url, byte_length)
return GaussianSourceUpload(
upload_id=upload_id,
filename=filename,
format=source_format,
sha256=sha256,
byte_length=byte_length,
)
def submit_build(self, document: Mapping[str, object]) -> dict[str, Any]:
if document.get("schema_version") != BUILD_REQUEST_SCHEMA:
raise GaussianPipelineGatewayError("Gaussian build request schema is invalid")
result = self._json("POST", "/v1/jobs", document=document)
if result.get("schema_version") != JOB_SCHEMA:
raise GaussianPipelineGatewayError("Gaussian job response does not match v1")
return result
def get_job(self, job_id: str) -> dict[str, Any]:
_safe_id(job_id, "job id")
result = self._json("GET", f"/v1/jobs/{quote(job_id, safe='')}")
if result.get("schema_version") != JOB_SCHEMA or result.get("job_id") != job_id:
raise GaussianPipelineGatewayError("Gaussian job identity does not match")
return result
def get_result(self, job_id: str) -> dict[str, Any]:
_safe_id(job_id, "job id")
result = self._json("GET", f"/v1/jobs/{quote(job_id, safe='')}/result")
if result.get("schema_version") != RESULT_SCHEMA or result.get("job_id") != job_id:
raise GaussianPipelineGatewayError("Gaussian result identity does not match")
return result
def download_artifact(
self,
job_id: str,
descriptor: Mapping[str, object],
destination: Path,
) -> Path:
_safe_id(job_id, "job id")
logical_path = descriptor.get("logical_path")
expected_sha256 = descriptor.get("sha256")
expected_bytes = descriptor.get("byte_length")
if (
not isinstance(logical_path, str)
or not logical_path
or logical_path.startswith("/")
or "\\" in logical_path
or any(part in {"", ".", ".."} for part in logical_path.split("/"))
):
raise GaussianPipelineGatewayError("Gaussian artifact logical path is invalid")
if (
not isinstance(expected_sha256, str)
or SHA256_PATTERN.fullmatch(expected_sha256) is None
):
raise GaussianPipelineGatewayError("Gaussian artifact digest is invalid")
if (
not isinstance(expected_bytes, int)
or isinstance(expected_bytes, bool)
or expected_bytes < 0
):
raise GaussianPipelineGatewayError("Gaussian artifact byte length is invalid")
target = destination.expanduser().resolve()
target.parent.mkdir(parents=True, exist_ok=True)
temporary = target.with_name(f".{target.name}.partial-{os.getpid()}")
digest = hashlib.sha256()
byte_length = 0
temporary_created = False
try:
with self._client.stream(
"GET",
f"/v1/jobs/{quote(job_id, safe='')}/artifacts/{quote(logical_path, safe='/')}",
) as response:
response.raise_for_status()
with temporary.open("xb") as output:
temporary_created = True
for chunk in response.iter_bytes(chunk_size=1024 * 1024):
output.write(chunk)
digest.update(chunk)
byte_length += len(chunk)
if byte_length != expected_bytes or digest.hexdigest() != expected_sha256:
raise GaussianPipelineIntegrityError(
"Gaussian artifact bytes do not match the result manifest"
)
temporary.replace(target)
return target
except httpx.HTTPError as exc:
raise _unavailable("Gaussian artifact download failed", exc) from exc
finally:
if temporary_created:
temporary.unlink(missing_ok=True)
def _send_file(self, source: Path, upload_url: str, byte_length: int) -> None:
offset = self._upload_offset(upload_url, byte_length)
retries = 0
with source.open("rb") as input_file:
while offset < byte_length:
input_file.seek(offset)
chunk = input_file.read(min(self.chunk_bytes, byte_length - offset))
if not chunk:
raise GaussianPipelineIntegrityError(
"Gaussian source ended before upload length"
)
try:
response = self._client.patch(
upload_url,
headers={
"Tus-Resumable": TUS_VERSION,
"Upload-Offset": str(offset),
"Content-Type": "application/offset+octet-stream",
},
content=chunk,
)
response.raise_for_status()
next_offset = _offset(response.headers.get("Upload-Offset"), byte_length)
if next_offset != offset + len(chunk):
raise GaussianPipelineGatewayError(
"Gaussian upload returned a non-contiguous offset"
)
offset = next_offset
retries = 0
except httpx.TransportError as exc:
retries += 1
if retries > MAX_RETRIES:
raise _unavailable(
"Gaussian upload failed after resume attempts", exc
) from exc
offset = self._upload_offset(upload_url, byte_length)
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 409 and retries < MAX_RETRIES:
retries += 1
offset = self._upload_offset(upload_url, byte_length)
continue
raise _unavailable("Gaussian upload was rejected", exc) from exc
def _upload_offset(self, upload_url: str, byte_length: int) -> int:
try:
response = self._client.head(
upload_url,
headers={"Tus-Resumable": TUS_VERSION},
)
response.raise_for_status()
except httpx.HTTPError as exc:
raise _unavailable("Gaussian upload offset is unavailable", exc) from exc
return _offset(response.headers.get("Upload-Offset"), byte_length)
def _json(
self,
method: str,
path: str,
*,
document: Mapping[str, object] | None = None,
) -> dict[str, Any]:
try:
response = self._client.request(method, path, json=document)
response.raise_for_status()
except httpx.HTTPError as exc:
raise _unavailable("Gaussian provider request failed", exc) from exc
if len(response.content) > MAX_JSON_RESPONSE_BYTES:
raise GaussianPipelineGatewayError("Gaussian provider JSON response is too large")
try:
value = response.json()
except json.JSONDecodeError as exc:
raise GaussianPipelineGatewayError("Gaussian provider response is not JSON") from exc
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise GaussianPipelineGatewayError("Gaussian provider response must be an object")
return value
def configured_gaussian_pipeline_gateway() -> GaussianPipelineGateway | None:
endpoint = os.environ.get("MISSIONCORE_GAUSSIAN_ENDPOINT")
token_file = os.environ.get("MISSIONCORE_GAUSSIAN_TOKEN_FILE")
if endpoint is None and token_file is None:
return None
if not endpoint or not token_file:
raise GaussianPipelineConfigurationError(
"MISSIONCORE_GAUSSIAN_ENDPOINT and MISSIONCORE_GAUSSIAN_TOKEN_FILE must be set together"
)
return GaussianPipelineGateway(endpoint, Path(token_file))
def _endpoint(value: str) -> str:
parsed = urlparse(value)
if (
parsed.scheme not in {"http", "https"}
or not parsed.hostname
or parsed.username is not None
or parsed.password is not None
or parsed.path not in {"", "/"}
or parsed.query
or parsed.fragment
):
raise GaussianPipelineConfigurationError("Gaussian provider endpoint is invalid")
return value.rstrip("/")
def _is_provider_upload_url(endpoint: str, upload_url: str) -> bool:
provider = urlparse(endpoint)
upload = urlparse(upload_url)
return (
upload.scheme == provider.scheme
and upload.hostname == provider.hostname
and upload.port == provider.port
and upload.username is None
and upload.password is None
and upload.path.startswith("/v1/uploads/")
and not upload.params
and not upload.query
and not upload.fragment
)
def _read_token(token_file: Path) -> str:
try:
metadata = token_file.lstat()
if token_file.is_symlink() or not token_file.is_file():
raise GaussianPipelineConfigurationError("Gaussian token must be one regular file")
token = token_file.read_text(encoding="utf-8").strip()
except OSError as exc:
raise GaussianPipelineConfigurationError("Gaussian token file is unavailable") from exc
if metadata.st_size > 4096 or not 32 <= len(token) <= 4096:
raise GaussianPipelineConfigurationError("Gaussian token length is invalid")
return token
def _tus_metadata(values: Mapping[str, str]) -> str:
return ",".join(
f"{key} {base64.b64encode(value.encode('utf-8')).decode('ascii')}"
for key, value in values.items()
)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _offset(value: str | None, byte_length: int) -> int:
if value is None or not value.isdigit():
raise GaussianPipelineGatewayError("Gaussian upload offset is invalid")
offset = int(value)
if not 0 <= offset <= byte_length:
raise GaussianPipelineGatewayError("Gaussian upload offset is outside the source")
return offset
def _safe_id(value: str, label: str) -> None:
if SAFE_UPLOAD_ID.fullmatch(value) is None:
raise GaussianPipelineGatewayError(f"Gaussian {label} is invalid")
def _unavailable(message: str, error: httpx.HTTPError) -> GaussianPipelineGatewayError:
if isinstance(error, httpx.TransportError):
return GaussianPipelineUnavailableError(message)
return GaussianPipelineGatewayError(message)
@@ -0,0 +1,66 @@
"""Read-only Mission Core projection of the external Gaussian build provider."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any, Literal
from fastapi import APIRouter
from pydantic import BaseModel, ConfigDict
from k1link.simulation.gaussian_pipeline_gateway import (
GaussianPipelineGateway,
GaussianPipelineGatewayError,
configured_gaussian_pipeline_gateway,
)
ProviderFactory = Callable[[], GaussianPipelineGateway | None]
class SimulationWorldProviderStatus(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
schema_version: Literal["missioncore.simulation-world-provider-status/v1"] = (
"missioncore.simulation-world-provider-status/v1"
)
provider_id: Literal["gaussian-pipeline"] = "gaussian-pipeline"
configured: bool
state: Literal["ready", "unavailable"]
capabilities: dict[str, Any] | None
def build_simulation_world_provider_router(
provider_factory: ProviderFactory = configured_gaussian_pipeline_gateway,
) -> APIRouter:
router = APIRouter()
@router.get(
"/api/v1/simulation-worlds/provider",
response_model=SimulationWorldProviderStatus,
)
def provider_status() -> SimulationWorldProviderStatus:
provider: GaussianPipelineGateway | None = None
try:
provider = provider_factory()
if provider is None:
return SimulationWorldProviderStatus(
configured=False,
state="unavailable",
capabilities=None,
)
return SimulationWorldProviderStatus(
configured=True,
state="ready",
capabilities=provider.capabilities(),
)
except GaussianPipelineGatewayError:
return SimulationWorldProviderStatus(
configured=True,
state="unavailable",
capabilities=None,
)
finally:
if provider is not None:
provider.close()
return router
+203
View File
@@ -0,0 +1,203 @@
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import Path
import httpx
import pytest
from k1link.simulation.gaussian_pipeline_gateway import (
BUILD_REQUEST_SCHEMA,
GaussianPipelineGateway,
GaussianPipelineGatewayError,
GaussianPipelineIntegrityError,
)
def _token_file(tmp_path: Path) -> Path:
token = tmp_path / "gaussian.token"
token.write_text("t" * 64, encoding="utf-8")
return token
def test_gateway_uploads_with_tus_and_reads_provider_contract(tmp_path: Path) -> None:
uploaded = bytearray()
metadata: dict[str, str] = {}
def handler(request: httpx.Request) -> httpx.Response:
assert request.headers["authorization"] == f"Bearer {'t' * 64}"
if request.method == "GET" and request.url.path == "/v1/capabilities":
return httpx.Response(
200,
json={
"schema_version": "gaussian-pipeline.capabilities/v1",
"service": "ndc-gaussian-pipeline",
"provider": {"id": "playcanvas-splat-transform", "version": "3.3.3"},
},
)
if request.method == "POST" and request.url.path == "/v1/uploads":
for item in request.headers["upload-metadata"].split(","):
key, encoded = item.split(" ", 1)
metadata[key] = base64.b64decode(encoded).decode("utf-8")
return httpx.Response(201, headers={"Location": "/v1/uploads/upload-001"})
if request.method == "HEAD" and request.url.path == "/v1/uploads/upload-001":
return httpx.Response(200, headers={"Upload-Offset": str(len(uploaded))})
if request.method == "PATCH" and request.url.path == "/v1/uploads/upload-001":
assert int(request.headers["upload-offset"]) == len(uploaded)
uploaded.extend(request.content)
return httpx.Response(204, headers={"Upload-Offset": str(len(uploaded))})
return httpx.Response(404)
source = tmp_path / "yard.lcc2"
source.write_bytes(b"portable-gaussian-source")
digest = hashlib.sha256(source.read_bytes()).hexdigest()
with GaussianPipelineGateway(
"http://gaussian.test",
_token_file(tmp_path),
chunk_bytes=5,
transport=httpx.MockTransport(handler),
) as gateway:
assert gateway.capabilities()["service"] == "ndc-gaussian-pipeline"
descriptor = gateway.upload_source(
source,
filename="yard.lcc2",
source_format="lcc2",
sha256=digest,
)
assert bytes(uploaded) == source.read_bytes()
assert metadata == {"filename": "yard.lcc2", "format": "lcc2", "sha256": digest}
assert descriptor.to_dict() == {
"upload_id": "upload-001",
"filename": "yard.lcc2",
"format": "lcc2",
"sha256": digest,
"byte_length": len(uploaded),
}
def test_gateway_rejects_local_source_digest_mismatch(tmp_path: Path) -> None:
source = tmp_path / "yard.lcc"
source.write_bytes(b"source")
with (
GaussianPipelineGateway(
"http://gaussian.test",
_token_file(tmp_path),
transport=httpx.MockTransport(lambda _request: httpx.Response(500)),
) as gateway,
pytest.raises(GaussianPipelineIntegrityError, match="digest does not match"),
):
gateway.upload_source(
source,
filename="yard.lcc",
source_format="lcc",
sha256="a" * 64,
)
def test_gateway_rejects_upload_location_outside_provider(tmp_path: Path) -> None:
source = tmp_path / "yard.lcc"
source.write_bytes(b"source")
digest = hashlib.sha256(source.read_bytes()).hexdigest()
def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(
201,
headers={"Location": "https://attacker.invalid/v1/uploads/stolen"},
)
with (
GaussianPipelineGateway(
"http://gaussian.test",
_token_file(tmp_path),
transport=httpx.MockTransport(handler),
) as gateway,
pytest.raises(GaussianPipelineGatewayError, match="escaped the provider"),
):
gateway.upload_source(
source,
filename="yard.lcc",
source_format="lcc",
sha256=digest,
)
def test_gateway_rejects_symlink_source_and_token(tmp_path: Path) -> None:
source = tmp_path / "source.lcc"
source.write_bytes(b"source")
source_link = tmp_path / "source-link.lcc"
source_link.symlink_to(source)
token = _token_file(tmp_path)
token_link = tmp_path / "token-link"
token_link.symlink_to(token)
with pytest.raises(GaussianPipelineGatewayError, match="token must be one regular file"):
GaussianPipelineGateway("http://gaussian.test", token_link)
digest = hashlib.sha256(source.read_bytes()).hexdigest()
with (
GaussianPipelineGateway(
"http://gaussian.test",
token,
transport=httpx.MockTransport(lambda _request: httpx.Response(500)),
) as gateway,
pytest.raises(GaussianPipelineIntegrityError, match="one regular file"),
):
gateway.upload_source(
source_link,
filename="source.lcc",
source_format="lcc",
sha256=digest,
)
def test_gateway_submits_tracks_and_imports_digest_bound_artifact(tmp_path: Path) -> None:
artifact = b"preview-sog"
artifact_sha = hashlib.sha256(artifact).hexdigest()
job_id = "gsp-20260825200000-deadbeef"
request_document: dict[str, object] = {
"schema_version": BUILD_REQUEST_SCHEMA,
"idempotency_key": "missioncore-build-01",
}
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "POST" and request.url.path == "/v1/jobs":
assert json.loads(request.content) == request_document
return httpx.Response(
202,
json={"schema_version": "gaussian-pipeline.job/v1", "job_id": job_id},
)
if request.method == "GET" and request.url.path == f"/v1/jobs/{job_id}":
return httpx.Response(
200,
json={"schema_version": "gaussian-pipeline.job/v1", "job_id": job_id},
)
if request.method == "GET" and request.url.path == f"/v1/jobs/{job_id}/result":
return httpx.Response(
200,
json={"schema_version": "gaussian-pipeline.build-result/v1", "job_id": job_id},
)
if request.method == "GET" and request.url.path.endswith("/artifacts/preview.sog"):
return httpx.Response(200, content=artifact)
return httpx.Response(404)
with GaussianPipelineGateway(
"http://gaussian.test",
_token_file(tmp_path),
transport=httpx.MockTransport(handler),
) as gateway:
assert gateway.submit_build(request_document)["job_id"] == job_id
assert gateway.get_job(job_id)["job_id"] == job_id
assert gateway.get_result(job_id)["job_id"] == job_id
destination = gateway.download_artifact(
job_id,
{
"logical_path": "preview.sog",
"sha256": artifact_sha,
"byte_length": len(artifact),
},
tmp_path / "import" / "preview.sog",
)
assert destination.read_bytes() == artifact
@@ -0,0 +1,66 @@
from __future__ import annotations
from pathlib import Path
import httpx
from fastapi import FastAPI
from fastapi.testclient import TestClient
from k1link.simulation.gaussian_pipeline_gateway import GaussianPipelineGateway
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_router
def _gateway(tmp_path: Path, status: int = 200) -> GaussianPipelineGateway:
token = tmp_path / "token"
token.write_text("x" * 64, encoding="utf-8")
def handler(_request: httpx.Request) -> httpx.Response:
if status != 200:
return httpx.Response(status)
return httpx.Response(
200,
json={
"schema_version": "gaussian-pipeline.capabilities/v1",
"service": "ndc-gaussian-pipeline",
"provider": {"id": "playcanvas-splat-transform", "version": "3.3.3"},
},
)
return GaussianPipelineGateway(
"http://gaussian.test",
token,
transport=httpx.MockTransport(handler),
)
def test_provider_status_is_explicitly_unconfigured() -> None:
app = FastAPI()
app.include_router(build_simulation_world_provider_router(lambda: None))
response = TestClient(app).get("/api/v1/simulation-worlds/provider")
assert response.status_code == 200
assert response.json() == {
"schema_version": "missioncore.simulation-world-provider-status/v1",
"provider_id": "gaussian-pipeline",
"configured": False,
"state": "unavailable",
"capabilities": None,
}
def test_provider_status_projects_ready_capabilities(tmp_path: Path) -> None:
app = FastAPI()
app.include_router(build_simulation_world_provider_router(lambda: _gateway(tmp_path)))
response = TestClient(app).get("/api/v1/simulation-worlds/provider")
assert response.status_code == 200
assert response.json()["state"] == "ready"
assert response.json()["capabilities"]["provider"]["version"] == "3.3.3"
def test_provider_status_fails_closed_without_leaking_transport_error(tmp_path: Path) -> None:
app = FastAPI()
app.include_router(build_simulation_world_provider_router(lambda: _gateway(tmp_path, 503)))
response = TestClient(app).get("/api/v1/simulation-worlds/provider")
assert response.status_code == 200
assert response.json()["configured"] is True
assert response.json()["state"] == "unavailable"
assert response.json()["capabilities"] is None