fix: stabilize test suite and repair frontend/backend wiring

- meshcore pin >=2.3.0 (EventType.STATS_CORE floor); setup.sh derives
  optional packages from requirements.txt; Python 3.10 warning
- agent-mode wifi clients proxy route + bare-array response handling
- remove dead AIS/ACARS/VDL2 SPA wiring and orphaned partials/CSS
- agent TLE download to data/tle/ (was littering repo root as gp.php)
- gate deferred background init off under pytest (mock-pollution race)
- complete Popen mocks (context manager protocol, communicate tuples)
- real pipe fds in weather-sat decoder tests (fd 10/11 collision caused
  10s SQLite stalls); satellite tests no longer rewrite data/satellites.py
- register 'live' pytest marker, excluded by default
- update stale test assertions to current APIs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-06-11 16:42:33 +01:00
parent b68a53eb53
commit d4652017f5
27 changed files with 3128 additions and 4029 deletions
+53 -67
View File
@@ -8,7 +8,7 @@ import logging
import requests
logger = logging.getLogger('intercept.agent_client')
logger = logging.getLogger("intercept.agent_client")
class AgentHTTPError(RuntimeError):
@@ -21,18 +21,14 @@ class AgentHTTPError(RuntimeError):
class AgentConnectionError(AgentHTTPError):
"""Exception raised when agent is unreachable."""
pass
class AgentClient:
"""HTTP client for communicating with a remote Intercept agent."""
def __init__(
self,
base_url: str,
api_key: str | None = None,
timeout: float = 60.0
):
def __init__(self, base_url: str, api_key: str | None = None, timeout: float = 60.0):
"""
Initialize agent client.
@@ -41,15 +37,15 @@ class AgentClient:
api_key: Optional API key for authentication
timeout: Request timeout in seconds
"""
self.base_url = base_url.rstrip('/')
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
def _headers(self) -> dict:
"""Get request headers."""
headers = {'Content-Type': 'application/json'}
headers = {"Content-Type": "application/json"}
if self.api_key:
headers['X-API-Key'] = self.api_key
headers["X-API-Key"] = self.api_key
return headers
def _get(self, path: str, params: dict | None = None) -> dict:
@@ -69,12 +65,7 @@ class AgentClient:
"""
url = f"{self.base_url}{path}"
try:
response = requests.get(
url,
headers=self._headers(),
params=params,
timeout=self.timeout
)
response = requests.get(url, headers=self._headers(), params=params, timeout=self.timeout)
response.raise_for_status()
return response.json() if response.content else {}
except requests.ConnectionError as e:
@@ -86,17 +77,17 @@ class AgentClient:
error_msg = f"Agent returned error: {e.response.status_code}"
try:
error_data = e.response.json()
if 'message' in error_data:
error_msg = error_data['message']
elif 'error' in error_data:
error_msg = error_data['error']
if "message" in error_data:
error_msg = error_data["message"]
elif "error" in error_data:
error_msg = error_data["error"]
except Exception:
pass
raise AgentHTTPError(error_msg, status_code=e.response.status_code)
except requests.RequestException as e:
raise AgentHTTPError(f"Request failed: {e}")
def _post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
def _post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
"""
Perform POST request to agent.
@@ -111,39 +102,38 @@ class AgentClient:
AgentHTTPError: On HTTP errors
AgentConnectionError: If agent is unreachable
"""
url = f"{self.base_url}{path}"
request_timeout = self.timeout if timeout is None else timeout
try:
response = requests.post(
url,
json=data or {},
headers=self._headers(),
timeout=request_timeout
)
response.raise_for_status()
return response.json() if response.content else {}
except requests.ConnectionError as e:
raise AgentConnectionError(f"Cannot connect to agent at {self.base_url}: {e}")
except requests.Timeout:
raise AgentConnectionError(f"Request to agent timed out after {request_timeout}s")
url = f"{self.base_url}{path}"
request_timeout = self.timeout if timeout is None else timeout
try:
response = requests.post(url, json=data or {}, headers=self._headers(), timeout=request_timeout)
response.raise_for_status()
return response.json() if response.content else {}
except requests.ConnectionError as e:
raise AgentConnectionError(f"Cannot connect to agent at {self.base_url}: {e}")
except requests.Timeout:
raise AgentConnectionError(f"Request to agent timed out after {request_timeout}s")
except requests.HTTPError as e:
# Try to extract error message from response body
error_msg = f"Agent returned error: {e.response.status_code}"
try:
error_data = e.response.json()
if 'message' in error_data:
error_msg = error_data['message']
elif 'error' in error_data:
error_msg = error_data['error']
if "message" in error_data:
error_msg = error_data["message"]
elif "error" in error_data:
error_msg = error_data["error"]
except Exception:
pass
raise AgentHTTPError(error_msg, status_code=e.response.status_code)
except requests.RequestException as e:
raise AgentHTTPError(f"Request failed: {e}")
def post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
"""Public POST method for arbitrary endpoints."""
return self._post(path, data, timeout=timeout)
def get(self, path: str, params: dict | None = None) -> dict:
"""Public GET method for arbitrary endpoints."""
return self._get(path, params)
def post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
"""Public POST method for arbitrary endpoints."""
return self._post(path, data, timeout=timeout)
# =========================================================================
# Capability & Status
@@ -156,7 +146,7 @@ class AgentClient:
Returns:
Dict with 'modes' (mode -> bool), 'devices' (list), 'agent_version'
"""
return self._get('/capabilities')
return self._get("/capabilities")
def get_status(self) -> dict:
"""
@@ -165,7 +155,7 @@ class AgentClient:
Returns:
Dict with 'running_modes', 'uptime', 'push_enabled', etc.
"""
return self._get('/status')
return self._get("/status")
def health_check(self) -> bool:
"""
@@ -175,14 +165,14 @@ class AgentClient:
True if agent is reachable and healthy
"""
try:
result = self._get('/health')
return result.get('status') == 'healthy'
result = self._get("/health")
return result.get("status") == "healthy"
except (AgentHTTPError, AgentConnectionError):
return False
def get_config(self) -> dict:
"""Get agent configuration (non-sensitive fields)."""
return self._get('/config')
return self._get("/config")
def update_config(self, **kwargs) -> dict:
"""
@@ -195,7 +185,7 @@ class AgentClient:
Returns:
Updated config
"""
return self._post('/config', kwargs)
return self._post("/config", kwargs)
# =========================================================================
# Mode Operations
@@ -212,9 +202,9 @@ class AgentClient:
Returns:
Start result with 'status' field
"""
return self._post(f'/{mode}/start', params or {})
return self._post(f"/{mode}/start", params or {})
def stop_mode(self, mode: str, timeout: float = 8.0) -> dict:
def stop_mode(self, mode: str, timeout: float = 8.0) -> dict:
"""
Stop a running mode on the agent.
@@ -224,7 +214,7 @@ class AgentClient:
Returns:
Stop result with 'status' field
"""
return self._post(f'/{mode}/stop', timeout=timeout)
return self._post(f"/{mode}/stop", timeout=timeout)
def get_mode_status(self, mode: str) -> dict:
"""
@@ -236,7 +226,7 @@ class AgentClient:
Returns:
Mode status with 'running' field
"""
return self._get(f'/{mode}/status')
return self._get(f"/{mode}/status")
def get_mode_data(self, mode: str) -> dict:
"""
@@ -248,7 +238,7 @@ class AgentClient:
Returns:
Data snapshot with 'data' field
"""
return self._get(f'/{mode}/data')
return self._get(f"/{mode}/data")
# =========================================================================
# Convenience Methods
@@ -262,17 +252,17 @@ class AgentClient:
Dict with capabilities, status, and config
"""
metadata = {
'capabilities': None,
'status': None,
'config': None,
'healthy': False,
"capabilities": None,
"status": None,
"config": None,
"healthy": False,
}
try:
metadata['capabilities'] = self.get_capabilities()
metadata['status'] = self.get_status()
metadata['config'] = self.get_config()
metadata['healthy'] = True
metadata["capabilities"] = self.get_capabilities()
metadata["status"] = self.get_status()
metadata["config"] = self.get_config()
metadata["healthy"] = True
except (AgentHTTPError, AgentConnectionError) as e:
logger.warning(f"Failed to refresh agent metadata: {e}")
@@ -292,8 +282,4 @@ def create_client_from_agent(agent: dict) -> AgentClient:
Returns:
Configured AgentClient
"""
return AgentClient(
base_url=agent['base_url'],
api_key=agent.get('api_key'),
timeout=60.0
)
return AgentClient(base_url=agent["base_url"], api_key=agent.get("api_key"), timeout=60.0)
+2 -13
View File
@@ -35,21 +35,10 @@ def is_meshcore_available() -> bool:
return HAS_MESHCORE
# Try to import ContactType for repeater detection
try:
from meshcore import ContactType as _ContactType
_REPEATER_TYPE = getattr(_ContactType, "REPEATER", None)
except Exception:
_ContactType = None
_REPEATER_TYPE = None
def _is_repeater_contact(contact_dict: dict) -> bool:
"""Return True if this contact is a repeater node."""
if _REPEATER_TYPE is not None:
return contact_dict.get("type") == _REPEATER_TYPE
# Fallback: meshcore repeaters have type==2 by convention
# meshcore exports no ContactType enum (checked through 2.3.7);
# repeaters have type==2 by library convention
return contact_dict.get("type") == 2
+421 -366
View File
File diff suppressed because it is too large Load Diff