Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/unit/client/test_vajra_tts_stream_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def _run_request(
monkeypatch: pytest.MonkeyPatch,
events: list[str],
):
monkeypatch.setattr(client, "_connect", lambda: _FakeConnection(websocket))
monkeypatch.setattr(client, "_connect", lambda *a, **k: _FakeConnection(websocket))
return asyncio.run(
client.send_request(
_request(),
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/evaluator/performance/test_lifecycle_drift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Unit tests for harness request-lifecycle drift metrics + soft WARN."""

import logging
from types import SimpleNamespace

import pytest

from veeksha.config.evaluator import PerformanceEvaluatorConfig
from veeksha.evaluator.performance.base import PerformanceEvaluator


def _response(ready, dispatched, pickup, sent):
return SimpleNamespace(
scheduler_ready_at=ready,
scheduler_dispatched_at=dispatched,
client_picked_up_at=pickup,
client_sent_at=sent,
)


def _evaluator(threshold_ms=5.0):
return PerformanceEvaluator(
PerformanceEvaluatorConfig(lifecycle_drift_warn_threshold_ms=threshold_ms)
)


@pytest.mark.unit
def test_lifecycle_metrics_in_summary():
ev = _evaluator()
for _ in range(20):
# ready->dispatch 1ms, dispatch->pickup 0.5ms, pickup->send 2ms
ev._accumulate_lifecycle_drift(_response(100.0, 100.001, 100.0015, 100.0035))

summary = ev.get_aggregated_summary()
assert "Harness Ready-to-Dispatch (ms) (P99)" in summary
assert summary["Harness Ready-to-Dispatch (ms) (P99)"] == pytest.approx(
1.0, rel=0.05
)
assert summary["Harness Pickup-to-Send (ms) (P99)"] == pytest.approx(2.0, rel=0.05)
assert summary["Harness Ready-to-Send (ms) (P99)"] == pytest.approx(3.5, rel=0.05)


@pytest.mark.unit
def test_missing_timestamps_are_skipped():
ev = _evaluator()
# client_sent_at missing -> pickup->send and ready->send have no samples
ev._accumulate_lifecycle_drift(_response(100.0, 100.001, 100.0015, None))
assert ev._lifecycle_sketches["Harness Ready-to-Dispatch (ms)"].sketch.count == 1
assert ev._lifecycle_sketches["Harness Pickup-to-Send (ms)"].sketch.count == 0
assert ev._lifecycle_sketches["Harness Ready-to-Send (ms)"].sketch.count == 0


@pytest.mark.unit
def test_warn_fires_above_threshold(caplog):
ev = _evaluator(threshold_ms=5.0)
for _ in range(30):
# pickup->send = 20ms, well above the 5ms threshold
ev._accumulate_lifecycle_drift(_response(100.0, 100.001, 100.0015, 100.0215))
with caplog.at_level(logging.WARNING):
ev._warn_on_lifecycle_drift()
warnings = [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING]
assert any("Pickup-to-Send" in w and "lifecycle drift high" in w for w in warnings)


@pytest.mark.unit
def test_no_warn_within_threshold(caplog):
ev = _evaluator(threshold_ms=50.0)
for _ in range(30):
ev._accumulate_lifecycle_drift(_response(100.0, 100.001, 100.0015, 100.0035))
with caplog.at_level(logging.WARNING):
ev._warn_on_lifecycle_drift()
assert not [r for r in caplog.records if "lifecycle drift high" in r.getMessage()]


@pytest.mark.unit
def test_warn_disabled_when_threshold_nonpositive(caplog):
ev = _evaluator(threshold_ms=0.0)
for _ in range(30):
ev._accumulate_lifecycle_drift(_response(100.0, 100.001, 100.0015, 100.5))
with caplog.at_level(logging.WARNING):
ev._warn_on_lifecycle_drift()
assert not [r for r in caplog.records if "lifecycle drift high" in r.getMessage()]
Empty file.
103 changes: 103 additions & 0 deletions tests/unit/preflight/test_chat_mock_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""End-to-end: real chat client -> spawned mock server -> scorer.

Exercises the actual OpenAIChatCompletionsClient streaming path against a
deterministic mock server running in a separate process, then scores the
paired client/server timestamps. This is the vertical slice's proof that
request/response delivery lag and server pacing fidelity are measured correctly.
"""

import asyncio

import pytest

from veeksha.client import ClientRegistry
from veeksha.config.client import OpenAIChatCompletionsClientConfig
from veeksha.core.request import Request
from veeksha.core.request_content import TextChannelRequestContent
from veeksha.core.tokenizer import TokenizerHandle, TokenizerProvider
from veeksha.preflight import scorer
from veeksha.preflight.spawn import spawn_mock_chat_server
from veeksha.types import ChannelModality

TTFC_MS = 80.0
TPOC_MS = 10.0
NUM_CHUNKS = 24


def _word_tokenizer_provider() -> TokenizerProvider:
handle = TokenizerHandle(
count_tokens=lambda text: len(str(text).split()),
decode=lambda token_ids: "",
encode=lambda text: [0] * len(str(text).split()),
)
return TokenizerProvider({ChannelModality.TEXT: handle})


def _make_request(rid: int) -> Request:
return Request(
id=rid,
channels={
ChannelModality.TEXT: TextChannelRequestContent(input_text=f"hello {rid}")
},
)


@pytest.mark.unit
def test_chat_client_against_mock_is_measured():
with spawn_mock_chat_server(
ttfc_ms=TTFC_MS, tpoc_ms=TPOC_MS, num_chunks=NUM_CHUNKS
) as server:
config = OpenAIChatCompletionsClientConfig(
api_base=server.api_base,
api_key="preflight",
model="dummy",
record_preflight_timing=True,
)
client = ClientRegistry.get(
config.get_type(),
config=config,
tokenizer_provider=_word_tokenizer_provider(),
)

async def _run():
results = []
for rid in range(6):
results.append(
await client.send_request(request=_make_request(rid), session_id=0)
)
return results

results = asyncio.run(_run())
server_records = server.fetch_records()

# every request succeeded and streamed all chunks
assert all(r.success for r in results), [r.error_msg for r in results]
assert all(len(r.chunk_recv_times) == NUM_CHUNKS for r in results)
assert len(server_records) == len(results)

report = scorer.score(results, server_records, ttfc_ms=TTFC_MS, tpoc_ms=TPOC_MS)

# all requests paired with a server record
assert report.n_paired_requests == len(results)
assert report.unpaired_fraction == 0.0

m = report.metrics
# delivery lags exist, are non-negative, and small on loopback
rd = m[scorer.M_REQUEST_DELIVERY]
resp = m[scorer.M_RESPONSE_DELIVERY]
assert rd.count == len(results)
assert resp.count == NUM_CHUNKS * len(results)
assert rd.minimum >= -0.001 # allow float noise; delivery should be >= 0
assert resp.minimum >= -0.001
# loopback delivery should be well under a few ms at p99
assert rd.p99 < 20.0
assert resp.p99 < 20.0

# the mock keeps its own schedule tightly (its whole job)
assert m[scorer.M_SERVER_TTFC_ABS_ERR].p99 < 15.0
assert m[scorer.M_SERVER_TPOC_ABS_ERR].p99 < 15.0

# client-observed ttfc/tpoc are in the right ballpark
assert m[scorer.M_CLIENT_TTFC].p50 == pytest.approx(TTFC_MS, abs=30.0)
assert m[scorer.M_CLIENT_TPOC].count == (NUM_CHUNKS - 1) * len(results)
assert m[scorer.M_CLIENT_TPOC].p50 == pytest.approx(TPOC_MS, abs=TPOC_MS)
171 changes: 171 additions & 0 deletions tests/unit/preflight/test_driver_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""End-to-end: the preflight driver runs the REAL benchmark loop vs a mock.

Unlike test_chat_mock_integration (which drives the client directly), this
exercises the full scheduler -> dispatch -> client_runner -> client path, so the
dispatch-drift metrics are populated too. Then it gates the result.
"""

import pytest

from veeksha.config.preflight import (
PreflightSttCheckConfig,
PreflightTextCheckConfig,
PreflightTtsCheckConfig,
)
from veeksha.config.traffic import ConcurrentTrafficConfig
from veeksha.preflight import scorer, validator
from veeksha.preflight.drivers import (
run_completions_preflight,
run_realtime_tts_preflight,
run_stt_preflight,
run_text_preflight,
run_tts_preflight,
run_vajra_tts_preflight,
)

TTFC_MS = 60.0
TPOC_MS = 8.0
NUM_CHUNKS = 16


def _traffic(concurrency):
return ConcurrentTrafficConfig(
target_concurrent_sessions=concurrency, rampup_seconds=0
)


def _text_cfg():
return PreflightTextCheckConfig(
input_tokens=8,
num_response_chunks=NUM_CHUNKS,
server_ttfc_ms=TTFC_MS,
server_tpoc_ms=TPOC_MS,
)


def _tts_cfg():
return PreflightTtsCheckConfig(
input_tokens=8,
input_chunk_tokens=2,
input_pacing_tps=500.0, # fast so the test isn't slow
num_response_chunks=NUM_CHUNKS,
server_ttfc_ms=TTFC_MS,
server_tpoc_ms=TPOC_MS,
)


def _stt_cfg():
return PreflightSttCheckConfig(
input_seconds=0.3, # short clip keeps the realtime-paced input quick
input_chunk_bytes=1024,
sample_rate=16000,
num_response_chunks=NUM_CHUNKS,
server_ttfc_ms=TTFC_MS,
server_tpoc_ms=TPOC_MS,
)


def _assert_measured(report, streaming_input):
assert report.n_requests > 0
assert report.n_paired_requests > 0
assert report.unpaired_fraction < 0.5
m = report.metrics
assert m[scorer.M_REQUEST_DELIVERY].count > 0
assert m[scorer.M_RESPONSE_DELIVERY].count > 0
assert m[scorer.M_SERVER_TTFC_ABS_ERR].count > 0
# dispatch drift only exists because the real scheduler/dispatch path ran.
assert m[scorer.M_LIFECYCLE_READY_TO_SEND].count > 0
if streaming_input:
assert m[scorer.M_INPUT_DELIVERY].count > 0
assert m[scorer.M_INPUT_PACING_ABS_ERR].count > 0
else:
assert scorer.M_INPUT_DELIVERY not in m
assert scorer.M_INPUT_PACING_ABS_ERR not in m


@pytest.mark.unit
def test_text_driver_runs_real_loop_and_scores(tmp_path):
report = run_text_preflight(
_text_cfg(),
traffic_scheduler=_traffic(8),
num_sessions=40,
output_dir=str(tmp_path / "text"),
)
_assert_measured(report, streaming_input=False)
assert report.metrics[scorer.M_SERVER_TPOC_ABS_ERR].count > 0

result = validator.run_validation(
report,
delivery_lag_threshold_ms=25.0,
server_pacing_threshold_ms=25.0,
dispatch_drift_threshold_ms=50.0,
input_pacing_threshold_ms=50.0,
max_unpaired_fraction=0.1,
)
assert result.verdict in (
validator.VERDICT_PASS,
validator.VERDICT_FAIL,
validator.VERDICT_SERVER_AT_CAPACITY,
)


@pytest.mark.unit
def test_completions_driver_runs_real_loop_and_scores(tmp_path):
report = run_completions_preflight(
_text_cfg(),
traffic_scheduler=_traffic(8),
num_sessions=40,
output_dir=str(tmp_path / "completions"),
)
_assert_measured(report, streaming_input=False)
# non-streaming: a single response, so no tpoc samples.
assert scorer.M_SERVER_TPOC_ABS_ERR not in report.metrics


@pytest.mark.unit
def test_tts_driver_runs_real_loop_and_scores(tmp_path):
report = run_tts_preflight(
_tts_cfg(),
traffic_scheduler=_traffic(8),
num_sessions=40,
output_dir=str(tmp_path / "tts"),
)
# HTTP tts sends the whole text in one POST -> no streaming-input metrics.
_assert_measured(report, streaming_input=False)
assert report.metrics[scorer.M_SERVER_TPOC_ABS_ERR].count > 0


@pytest.mark.unit
def test_realtime_tts_driver_runs_real_loop_and_scores(tmp_path):
report = run_realtime_tts_preflight(
_tts_cfg(),
traffic_scheduler=_traffic(6),
num_sessions=24,
output_dir=str(tmp_path / "realtime_tts"),
)
_assert_measured(report, streaming_input=True)
assert report.metrics[scorer.M_SERVER_TPOC_ABS_ERR].count > 0


@pytest.mark.unit
def test_vajra_tts_driver_runs_real_loop_and_scores(tmp_path):
report = run_vajra_tts_preflight(
_tts_cfg(),
traffic_scheduler=_traffic(6),
num_sessions=24,
output_dir=str(tmp_path / "vajra_tts"),
)
_assert_measured(report, streaming_input=True)
assert report.metrics[scorer.M_SERVER_TPOC_ABS_ERR].count > 0


@pytest.mark.unit
def test_stt_driver_runs_real_loop_and_scores(tmp_path):
report = run_stt_preflight(
_stt_cfg(),
traffic_scheduler=_traffic(4),
num_sessions=16,
output_dir=str(tmp_path / "stt"),
)
_assert_measured(report, streaming_input=True)
assert report.metrics[scorer.M_SERVER_TPOC_ABS_ERR].count > 0
Loading