mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Follow-up to #1265. Add coverage for the enabled branch of `periodic_toin_stats_enabled` during proxy lifespan startup. The original PR added the opt-out and disabled-path coverage. This test covers the default/enabled path so the new lifespan guard is not left partially covered. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added `test_lifespan_schedules_periodic_toin_stats_when_enabled`. - The test patches `_log_toin_stats_periodically` with a short noop coroutine and verifies the proxy lifespan requests it when `periodic_toin_stats_enabled=True`. - This complements the existing disabled-path test from #1265. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_proxy_telemetry_env.py -q ============================= test session starts ============================= platform win32 -- Python 3.11.15, pytest-9.0.3, pluggy-1.6.0 rootdir: C:\Users\wstcz\AppData\Local\Temp\headroom-main-20260622-073524 configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.8.0, asyncio-1.3.0, cov-7.0.0 asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 10 items tests\test_proxy_telemetry_env.py .......... [100%] ============================== warnings summary =============================== .venv\Lib\site-packages\fastapi\testclient.py:1 C:\Users\wstcz\AppData\Local\Temp\headroom-main-20260622-073524\.venv\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. from starlette.testclient import TestClient as TestClient # noqa -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ======================== 10 passed, 1 warning in 2.02s ======================== $ git diff --check origin/main..HEAD # no output; command exited 0 ``` ## Real Behavior Proof - Environment: Windows, Python 3.11.15 uv-managed `.venv`, branch based on current `origin/main`. - Exact command / steps: ran `uv run pytest tests/test_proxy_telemetry_env.py -q`. - Observed result: all 10 tests in `tests/test_proxy_telemetry_env.py` passed, including the enabled periodic TOIN stats lifespan branch. - Not tested: full repository pytest, ruff, and mypy were not run for this test-only follow-up. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - Test-only follow-up to #1265. - No production behavior changes. - The focused pytest run still emits the existing Starlette/FastAPI TestClient deprecation warning from dependencies, so `My changes generate no new warnings` is intentionally left unchecked.
150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
"""Tests for proxy telemetry environment variable handling."""
|
|
|
|
import asyncio
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("fastapi")
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from headroom.proxy.server import ProxyConfig, _proxy_config_from_env, create_app
|
|
|
|
|
|
class TestProxyTelemetrySDKEnv:
|
|
"""Test HEADROOM_SDK handling when the proxy builds telemetry beacons."""
|
|
|
|
def test_proxy_telemetry_sdk_defaults_to_proxy(self, monkeypatch):
|
|
"""Telemetry beacon uses the default SDK label when env var is unset."""
|
|
monkeypatch.delenv("HEADROOM_SDK", raising=False)
|
|
|
|
with patch("headroom.telemetry.beacon.TelemetryBeacon") as mock_beacon:
|
|
create_app(
|
|
ProxyConfig(
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
)
|
|
)
|
|
|
|
assert mock_beacon.call_args.kwargs["sdk"] == "proxy"
|
|
|
|
def test_proxy_telemetry_sdk_uses_env_override(self, monkeypatch):
|
|
"""Telemetry beacon uses HEADROOM_SDK when it is non-empty."""
|
|
monkeypatch.setenv("HEADROOM_SDK", "headroom-app")
|
|
|
|
with patch("headroom.telemetry.beacon.TelemetryBeacon") as mock_beacon:
|
|
create_app(
|
|
ProxyConfig(
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
)
|
|
)
|
|
|
|
assert mock_beacon.call_args.kwargs["sdk"] == "headroom-app"
|
|
|
|
def test_proxy_telemetry_sdk_empty_env_falls_back_to_proxy(self, monkeypatch):
|
|
"""Telemetry beacon falls back to proxy when HEADROOM_SDK is blank."""
|
|
monkeypatch.setenv("HEADROOM_SDK", " ")
|
|
|
|
with patch("headroom.telemetry.beacon.TelemetryBeacon") as mock_beacon:
|
|
create_app(
|
|
ProxyConfig(
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
)
|
|
)
|
|
|
|
assert mock_beacon.call_args.kwargs["sdk"] == "proxy"
|
|
|
|
|
|
class TestProxyPeriodicTOINStatsEnv:
|
|
"""Test HEADROOM_PERIODIC_TOIN_STATS handling for long-lived proxy workers."""
|
|
|
|
def test_periodic_toin_stats_enabled_by_default(self, monkeypatch):
|
|
"""Periodic TOIN stats logging remains enabled unless explicitly disabled."""
|
|
monkeypatch.delenv("HEADROOM_PERIODIC_TOIN_STATS", raising=False)
|
|
|
|
config = _proxy_config_from_env()
|
|
|
|
assert config.periodic_toin_stats_enabled is True
|
|
|
|
@pytest.mark.parametrize("value", ["0", "false", "off", "no"])
|
|
def test_periodic_toin_stats_can_be_disabled_by_env(self, monkeypatch, value):
|
|
"""HEADROOM_PERIODIC_TOIN_STATS=0/false/off/no disables periodic logging."""
|
|
monkeypatch.setenv("HEADROOM_PERIODIC_TOIN_STATS", value)
|
|
|
|
config = _proxy_config_from_env()
|
|
|
|
assert config.periodic_toin_stats_enabled is False
|
|
|
|
def test_lifespan_skips_periodic_toin_stats_when_disabled(self, monkeypatch):
|
|
"""Disabling periodic TOIN stats avoids scheduling the stats loop."""
|
|
monkeypatch.setenv("HEADROOM_SKIP_UPSTREAM_CHECK", "1")
|
|
requested = False
|
|
|
|
def fake_periodic_toin_stats():
|
|
nonlocal requested
|
|
requested = True
|
|
|
|
async def noop():
|
|
await asyncio.sleep(0)
|
|
|
|
return noop()
|
|
|
|
monkeypatch.setattr(
|
|
"headroom.proxy.server._log_toin_stats_periodically",
|
|
fake_periodic_toin_stats,
|
|
)
|
|
|
|
app = create_app(
|
|
ProxyConfig(
|
|
optimize=False,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
periodic_toin_stats_enabled=False,
|
|
)
|
|
)
|
|
|
|
with TestClient(app):
|
|
pass
|
|
|
|
assert requested is False
|
|
|
|
def test_lifespan_schedules_periodic_toin_stats_when_enabled(self, monkeypatch):
|
|
"""Enabled periodic TOIN stats schedules the stats loop at startup."""
|
|
monkeypatch.setenv("HEADROOM_SKIP_UPSTREAM_CHECK", "1")
|
|
requested = False
|
|
|
|
def fake_periodic_toin_stats():
|
|
nonlocal requested
|
|
requested = True
|
|
|
|
async def noop():
|
|
await asyncio.sleep(0)
|
|
|
|
return noop()
|
|
|
|
monkeypatch.setattr(
|
|
"headroom.proxy.server._log_toin_stats_periodically",
|
|
fake_periodic_toin_stats,
|
|
)
|
|
|
|
app = create_app(
|
|
ProxyConfig(
|
|
optimize=False,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
periodic_toin_stats_enabled=True,
|
|
)
|
|
)
|
|
|
|
with TestClient(app):
|
|
pass
|
|
|
|
assert requested is True
|