File size: 16,604 Bytes
45633b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
"""Tests for LLMLingua opt-in mechanism in the proxy server.

These tests verify:
- ProxyConfig llmlingua settings
- LLMLingua transform integration in pipeline
- Status detection and logging hints
- CLI flag parsing
- DevEx: helpful messages when llmlingua unavailable
"""

from unittest.mock import MagicMock, patch

import pytest

# Skip if fastapi not available
pytest.importorskip("fastapi")

from fastapi.testclient import TestClient

from headroom.proxy.server import (
    HeadroomProxy,
    ProxyConfig,
    _get_llmlingua_banner_status,
    create_app,
)
from headroom.transforms import _LLMLINGUA_AVAILABLE

# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def base_config():
    """Base config with optimization disabled for simpler tests."""
    return ProxyConfig(
        optimize=False,
        cache_enabled=False,
        rate_limit_enabled=False,
        cost_tracking_enabled=False,
    )


@pytest.fixture
def llmlingua_config():
    """Config with LLMLingua enabled."""
    return ProxyConfig(
        optimize=True,
        cache_enabled=False,
        rate_limit_enabled=False,
        cost_tracking_enabled=False,
        llmlingua_enabled=True,
        llmlingua_device="cpu",
        llmlingua_target_rate=0.4,
    )


@pytest.fixture
def client(base_config):
    """Create test client with base config."""
    app = create_app(base_config)
    with TestClient(app) as client:
        yield client


# =============================================================================
# TestProxyConfigLLMLingua
# =============================================================================


class TestProxyConfigLLMLingua:
    """Tests for LLMLingua settings in ProxyConfig."""

    def test_default_llmlingua_disabled(self):
        """LLMLingua is disabled by default."""
        config = ProxyConfig()

        assert config.llmlingua_enabled is False
        assert config.llmlingua_device == "auto"
        assert config.llmlingua_target_rate == 0.3

    def test_llmlingua_can_be_enabled(self):
        """LLMLingua can be enabled via config."""
        config = ProxyConfig(
            llmlingua_enabled=True,
            llmlingua_device="cuda",
            llmlingua_target_rate=0.5,
        )

        assert config.llmlingua_enabled is True
        assert config.llmlingua_device == "cuda"
        assert config.llmlingua_target_rate == 0.5

    def test_llmlingua_device_options(self):
        """LLMLingua device accepts valid options."""
        for device in ["auto", "cuda", "cpu", "mps"]:
            config = ProxyConfig(llmlingua_device=device)
            assert config.llmlingua_device == device

    def test_llmlingua_target_rate_range(self):
        """LLMLingua target rate accepts 0.0-1.0 range."""
        # Low rate (aggressive compression)
        config_low = ProxyConfig(llmlingua_target_rate=0.1)
        assert config_low.llmlingua_target_rate == 0.1

        # High rate (conservative compression)
        config_high = ProxyConfig(llmlingua_target_rate=0.8)
        assert config_high.llmlingua_target_rate == 0.8


# =============================================================================
# TestLLMLinguaSetup
# =============================================================================


class TestLLMLinguaSetup:
    """Tests for LLMLingua setup in HeadroomProxy."""

    def test_setup_returns_disabled_when_not_enabled(self, base_config):
        """Setup returns 'disabled' when llmlingua not enabled and not available."""
        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
            proxy = HeadroomProxy(base_config)
            assert proxy._llmlingua_status == "disabled"

    def test_setup_returns_available_when_installed_but_not_enabled(self, base_config):
        """Setup returns 'available' when llmlingua installed but not enabled."""
        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            proxy = HeadroomProxy(base_config)
            assert proxy._llmlingua_status == "available"

    def test_setup_returns_enabled_when_enabled_and_available(self, llmlingua_config):
        """Setup returns 'enabled' when llmlingua enabled and available."""
        mock_compressor = MagicMock()

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor):
                with patch("headroom.proxy.server.LLMLinguaConfig"):
                    proxy = HeadroomProxy(llmlingua_config)
                    assert proxy._llmlingua_status == "enabled"

    def test_setup_returns_unavailable_when_enabled_but_not_installed(self):
        """Setup returns 'unavailable' when enabled but llmlingua not installed."""
        config = ProxyConfig(
            llmlingua_enabled=True,
            optimize=False,
            cache_enabled=False,
            rate_limit_enabled=False,
        )

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
            proxy = HeadroomProxy(config)
            assert proxy._llmlingua_status == "unavailable"

    def test_llmlingua_compressor_added_to_pipeline(self, llmlingua_config):
        """LLMLinguaCompressor is added to pipeline when enabled."""
        mock_compressor_class = MagicMock()
        mock_compressor_instance = MagicMock()
        mock_compressor_class.return_value = mock_compressor_instance

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor_class):
                with patch("headroom.proxy.server.LLMLinguaConfig") as mock_config:
                    HeadroomProxy(llmlingua_config)

                    # Verify LLMLinguaCompressor was instantiated
                    mock_compressor_class.assert_called_once()

                    # Verify config was passed with correct device and rate
                    call_args = mock_config.call_args
                    assert call_args.kwargs["device"] == "cpu"
                    assert call_args.kwargs["target_compression_rate"] == 0.4

    def test_llmlingua_not_added_when_disabled(self, base_config):
        """LLMLinguaCompressor is NOT added when disabled."""
        mock_compressor_class = MagicMock()

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor_class):
                HeadroomProxy(base_config)

                # Should NOT be called when disabled
                mock_compressor_class.assert_not_called()


# =============================================================================
# TestBannerStatus
# =============================================================================


class TestBannerStatus:
    """Tests for banner status helper function."""

    def test_banner_disabled_when_not_available(self):
        """Banner shows DISABLED when llmlingua not available and not enabled."""
        config = ProxyConfig(llmlingua_enabled=False)

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
            status = _get_llmlingua_banner_status(config)
            assert status == "DISABLED"

    def test_banner_available_hint_when_installed(self):
        """Banner shows availability hint when installed but not enabled."""
        config = ProxyConfig(llmlingua_enabled=False)

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            status = _get_llmlingua_banner_status(config)
            assert "available" in status
            assert "--llmlingua" in status

    def test_banner_enabled_when_active(self):
        """Banner shows ENABLED with config when active."""
        config = ProxyConfig(
            llmlingua_enabled=True,
            llmlingua_device="cuda",
            llmlingua_target_rate=0.25,
        )

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            status = _get_llmlingua_banner_status(config)
            assert "ENABLED" in status
            assert "cuda" in status
            assert "0.25" in status

    def test_banner_shows_install_hint_when_requested_but_missing(self):
        """Banner shows install hint when enabled but not installed."""
        config = ProxyConfig(llmlingua_enabled=True)

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
            status = _get_llmlingua_banner_status(config)
            assert "not installed" in status
            assert "pip install" in status


# =============================================================================
# TestHealthEndpointWithLLMLingua
# =============================================================================


class TestHealthEndpointWithLLMLingua:
    """Tests for health endpoint reflecting LLMLingua status."""

    def test_health_returns_llmlingua_in_config(self, client):
        """Health endpoint works regardless of LLMLingua status."""
        response = client.get("/health")
        assert response.status_code == 200

        data = response.json()
        assert data["status"] == "healthy"
        assert "config" in data


# =============================================================================
# TestStatsEndpointWithLLMLingua
# =============================================================================


class TestStatsEndpointWithLLMLingua:
    """Tests for stats endpoint with LLMLingua integration."""

    def test_stats_endpoint_works(self, client):
        """Stats endpoint works with any LLMLingua configuration."""
        response = client.get("/stats")
        assert response.status_code == 200

        data = response.json()
        assert "requests" in data
        assert "tokens" in data


# =============================================================================
# TestCLIArguments
# =============================================================================


class TestCLIArguments:
    """Tests for CLI argument parsing (without actually running server)."""

    def test_llmlingua_flag_defaults(self):
        """Default CLI values for LLMLingua settings."""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--llmlingua", action="store_true")
        parser.add_argument("--llmlingua-device", default="auto")
        parser.add_argument("--llmlingua-rate", type=float, default=0.3)

        args = parser.parse_args([])

        assert args.llmlingua is False
        assert args.llmlingua_device == "auto"
        assert args.llmlingua_rate == 0.3

    def test_llmlingua_flag_enabled(self):
        """CLI --llmlingua flag enables LLMLingua."""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--llmlingua", action="store_true")
        parser.add_argument("--llmlingua-device", default="auto")
        parser.add_argument("--llmlingua-rate", type=float, default=0.3)

        args = parser.parse_args(["--llmlingua"])

        assert args.llmlingua is True

    def test_llmlingua_device_flag(self):
        """CLI --llmlingua-device flag sets device."""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--llmlingua-device", default="auto")

        args = parser.parse_args(["--llmlingua-device", "cuda"])

        assert args.llmlingua_device == "cuda"

    def test_llmlingua_rate_flag(self):
        """CLI --llmlingua-rate flag sets compression rate."""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--llmlingua-rate", type=float, default=0.3)

        args = parser.parse_args(["--llmlingua-rate", "0.5"])

        assert args.llmlingua_rate == 0.5


# =============================================================================
# TestDevExMessages
# =============================================================================


class TestDevExMessages:
    """Tests for developer experience messages and hints."""

    def test_warning_logged_when_enabled_but_unavailable(self, caplog):
        """Warning is logged when llmlingua enabled but not installed."""
        import logging

        config = ProxyConfig(
            llmlingua_enabled=True,
            optimize=False,
            cache_enabled=False,
            rate_limit_enabled=False,
        )

        with caplog.at_level(logging.WARNING):
            with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
                proxy = HeadroomProxy(config)

                # Should have logged a warning about missing llmlingua
                assert proxy._llmlingua_status == "unavailable"
                assert any("llmlingua" in r.message.lower() for r in caplog.records)
                assert any("pip install" in r.message for r in caplog.records)


# =============================================================================
# TestIntegrationWithActualLLMLingua
# =============================================================================


@pytest.mark.skipif(not _LLMLINGUA_AVAILABLE, reason="llmlingua not installed")
class TestIntegrationWithActualLLMLingua:
    """Integration tests that require actual llmlingua installation.

    These tests verify the full integration path when llmlingua is installed.
    """

    def test_proxy_starts_with_llmlingua_enabled(self):
        """Proxy starts successfully with LLMLingua enabled."""
        config = ProxyConfig(
            llmlingua_enabled=True,
            llmlingua_device="cpu",  # CPU for CI/test environments
            llmlingua_target_rate=0.3,
            optimize=True,
            cache_enabled=False,
            rate_limit_enabled=False,
        )

        # Should not raise
        proxy = HeadroomProxy(config)

        assert proxy._llmlingua_status == "enabled"

    def test_app_creates_with_llmlingua(self):
        """FastAPI app creates successfully with LLMLingua enabled."""
        config = ProxyConfig(
            llmlingua_enabled=True,
            llmlingua_device="cpu",
            optimize=True,
            cache_enabled=False,
            rate_limit_enabled=False,
        )

        # Should not raise
        app = create_app(config)

        assert app is not None

    def test_health_endpoint_with_llmlingua_enabled(self):
        """Health endpoint works with LLMLingua enabled."""
        config = ProxyConfig(
            llmlingua_enabled=True,
            llmlingua_device="cpu",
            optimize=True,
            cache_enabled=False,
            rate_limit_enabled=False,
        )

        app = create_app(config)
        with TestClient(app) as client:
            response = client.get("/health")
            assert response.status_code == 200


# =============================================================================
# TestEdgeCases
# =============================================================================


class TestEdgeCases:
    """Edge cases for LLMLingua proxy integration."""

    def test_multiple_proxy_instances_independent(self):
        """Multiple proxy instances have independent LLMLingua status."""
        config_enabled = ProxyConfig(
            llmlingua_enabled=True,
            optimize=False,
            cache_enabled=False,
            rate_limit_enabled=False,
        )
        config_disabled = ProxyConfig(
            llmlingua_enabled=False,
            optimize=False,
            cache_enabled=False,
            rate_limit_enabled=False,
        )

        with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
            with patch("headroom.proxy.server.LLMLinguaCompressor"):
                with patch("headroom.proxy.server.LLMLinguaConfig"):
                    proxy_enabled = HeadroomProxy(config_enabled)
                    proxy_disabled = HeadroomProxy(config_disabled)

                    assert proxy_enabled._llmlingua_status == "enabled"
                    assert proxy_disabled._llmlingua_status == "available"

    def test_config_immutable_after_proxy_creation(self, base_config):
        """Config values are captured at proxy creation time."""
        proxy = HeadroomProxy(base_config)

        # Modifying config after creation doesn't affect proxy
        # (ProxyConfig is a dataclass, so this tests the pattern)
        original_status = proxy._llmlingua_status

        # Status should remain unchanged
        assert proxy._llmlingua_status == original_status