unsloth/tests/studio/load_freeze/test_load_orchestrator.py
Leo Borcherding 1dd2fc4583
tests: read checked-in files as UTF-8 instead of the platform default (#7438)
* tests: read checked-in files as UTF-8 instead of the platform default

Path.read_text() with no encoding uses locale.getpreferredencoding(), which
is UTF-8 on the Linux runners and cp1252 on a stock Windows install. Nine
module-level reads of checked-in source files were relying on that default.

studio/backend/routes/inference.py carries the DeepSeek tool-call token
regexes, so it holds U+FF5C and U+2581. Under cp1252 that read raised
UnicodeDecodeError on byte 0x81 at position 97806, and because the reads run
at import time it took test_cancel_atomicity.py and test_cancel_id_wiring.py
out at collection, not as failures. Green on CI, permanently broken for a
Windows contributor running the suite locally.

Adds a guard: at module scope there is no tmp_path fixture, so a bare
read_text()/write_text()/open() there is always touching a checked-in file.
That makes the rule mechanical enough to enforce with no allowlist, while
staying quiet about temp-dir I/O inside test bodies where the platform
default is harmless.

The repo already spells this correctly in 464 other places; this only stops
the stragglers coming back.

* tests: cover import-time helper reads and keep the guard py3.9-safe

Follows up on the Codex review:

- add `from __future__ import annotations`, since `str | None` in
  `_offender` is evaluated at import on Python 3.9 and pyproject declares
  requires-python ">=3.9,<3.15".
- widen the guard from module scope to import time. Class bodies and the
  bodies of module-level helpers called from an executing statement run
  during collection too, so `CODE = _extract_mixed_precision_code()` was
  the same hazard as an inline read. `if __name__ == "__main__":` blocks
  are skipped: pytest never executes them.
- scan studio/backend/tests/ as well as tests/. Both trees are collected
  on Windows by separate CI jobs, and the offender that started this,
  test_tool_xml_strip.py reading routes/inference.py, lives there.

Widening it surfaced seven more import-time reads of checked-in sources;
all now name utf-8.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Harden the import-time encoding guard for PR #7438

Close the detector gaps raised in review, all of which I reproduced against
the actual AST before changing anything.

False negatives (the guard let a real hazard through):
- _is_main_guard ignored the comparison operator, so if __name__ != "__main__"
  counted as script-only even though its body runs at import.
- The else arm of a main guard was discarded with the rest of the If node.
- Decorators and argument defaults on a module-level def were skipped with the
  body, though both are evaluated when the def executes.
- Path.open() in text mode was invisible; only builtin open() was matched.
- encoding = None and encoding = "locale" both re-select the platform default,
  but the keyword merely being present counted as pinned.

False positives (the guard would have blocked a compliant contributor):
- A non-literal mode fell through to the "r" default, so open(p, mode) was
  flagged even when mode is "rb", where adding encoding= is a ValueError and
  there is no edit that satisfies the rule.
- Same for open(*args) and a **kwargs splat, which hide the mode and can hide
  an encoding.
- Lambda bodies and comprehension elements were walked even though neither runs
  at definition.

Verified: still reports the same 22 offenders on unpatched main, green on this
branch and on the tree merged with latest main (557 files), and an adversarial
corpus of 33 cases now scores zero false positives and zero false negatives.
Also corrected two docstring claims: neither collecting job runs on Windows,
and the read is governed by locale.getencoding().

* Walk eager comprehensions and treat io.open as the builtin

Two regressions from the previous commit, both reproduced against the AST
before changing anything.

Lumping list, set and dict comprehensions in with generator expressions was
wrong. Only a genexp is lazy; the other three run their element expression,
their filters and their nested iterators immediately, so
CONTENTS = [p.read_text() for p in PATHS] at module scope is an import-time
read the guard was silently missing. Comprehensions are now walked in full and
only the genexp keeps the outermost-iterable-only treatment.

io was also in the not-a-path-opener list, but io.open is the builtin, with the
same mode position and the same platform default. io.open(CHECKED_IN_FILE) is
exactly the hazard this guard exists for, so it is matched now, with binary
modes and a pinned encoding still exempt. tarfile.open and fitz.open stay
exempt since neither has an encoding to name.

Verified: 13 targeted cases covering all five eager comprehension forms and
io.open in text, binary and pinned shapes all classify correctly; still 22
offenders on unpatched main; green on this branch and on the tree merged with
latest main.

* Close three more walker gaps in the import-time guard

All three reproduced against the AST first.

A generator expression handed straight to a call is consumed there, so
DATA = "".join(p.read_text() for p in paths) runs its element at import. Only
an unconsumed genexp bound to a name stays lazy, so the walker now follows the
consumed ones in full and keeps the outermost-iterable-only treatment for the
rest.

if "__main__" == __name__ is an equivalent and accepted spelling of the main
guard, but requiring __name__ on the left meant its body was treated as
import-time code. That is a false positive on a block pytest never runs, so
both operand orders are recognised now.

The helper table was built from module-level defs only, so a def in a class
body invoked while the class is constructed was never followed, contradicting
the walker's stated coverage of class bodies. Helpers are now collected from
the module body and from class bodies at any nesting.

Verified: 15 targeted cases including all three fixes and the earlier ones
still classify correctly; still 22 offenders on unpatched main; green on this
branch and on the tree merged with latest main.

* Handle positional read_text encodings, lazy generators and nested helpers

* Guard reads reached from test bodies, unbound Path calls and __file__ paths

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Follow derived paths, skip lazy generator helpers, cover compressed openers

* Guard the CLI tests, helper parameters and unbound Path arguments

* Discover test roots and follow literal, in-place and tuple-derived paths

* Identify module openers by import, unwrap starred paths, pin subprocess snippets

* Resolve import origins, seed helper locals, follow named generators and parametrize

* Scope imports lexically, list tracked test files, bind unpacked names

* Resolve aliased openers, keyword-only params, destructured targets, next()

* Pin the encoding on subprocess snippets, workflow lint and CLI output for PR #7438

* Harden the CLI encoding guard against detached streams for PR #7438

* Tighten the encoding guard's path and scope analysis for PR #7438

* Resolve path provenance more precisely and keep POSIX stream encodings for PR #7438

* Resolve qualified path classes and scope conditional imports for PR #7438

* Scope CLI stream setup to the entry point and align two encoding pairs for PR #7438

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-26 23:31:56 -07:00

602 lines
22 KiB
Python

"""Simulation suite for the #5642 fix (sync detect_audio_type blocking the event loop)."""
from __future__ import annotations
import asyncio
import os
import re
import socket
import sys
import threading
import time
import types
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import pytest
# Repo discovery
def _find_repo_root() -> Path | None:
env = os.environ.get("UNSLOTH_REPO_ROOT")
if env:
p = Path(env).resolve()
if (p / "studio" / "backend").is_dir():
return p
here = Path(__file__).resolve()
for parent in (here, *here.parents):
if (parent / "studio" / "backend").is_dir():
return parent
if (parent / "unsloth" / "studio" / "backend").is_dir():
return parent / "unsloth"
return None
_REPO_ROOT = _find_repo_root()
if _REPO_ROOT is None:
pytest.skip(
"Could not locate studio/backend. Set UNSLOTH_REPO_ROOT or clone "
"unslothai/unsloth into a parent directory.",
allow_module_level = True,
)
_STUDIO_BACKEND = _REPO_ROOT / "studio" / "backend"
sys.path.insert(0, str(_STUDIO_BACKEND))
sys.path.insert(0, str(Path(__file__).resolve().parent))
import logging as _logging # noqa: E402
_loggers_stub = types.ModuleType("loggers")
_loggers_stub.get_logger = lambda name: _logging.getLogger(name)
sys.modules.setdefault("loggers", _loggers_stub)
sys.modules.setdefault("structlog", types.ModuleType("structlog"))
import httpx # noqa: E402
from core.inference.llama_cpp import LlamaCppBackend # noqa: E402
from llama_server_shim import FakeLlamaServer # noqa: E402
# Fixtures / helpers
def _make_backend(port: int, *, loaded: bool = True) -> LlamaCppBackend:
b = LlamaCppBackend.__new__(LlamaCppBackend)
b._port = port
b._api_key = None
b._process = object() if loaded else None
b._healthy = loaded
return b
def _free_port() -> int:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
finally:
s.close()
class _UvicornServerThread:
def __init__(
self,
app,
*,
host: str = "127.0.0.1",
port: int,
) -> None:
import uvicorn
self.host = host
self.port = port
cfg = uvicorn.Config(app, host = host, port = port, log_level = "warning", access_log = False)
self._server = uvicorn.Server(cfg)
self._server.install_signal_handlers = lambda: None # type: ignore[assignment]
self._thread: threading.Thread | None = None
def start(self):
self._thread = threading.Thread(target = self._server.run, daemon = True)
self._thread.start()
self._wait_ready()
return self
def _wait_ready(self, timeout: float = 15.0) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
r = httpx.get(f"http://{self.host}:{self.port}/health", timeout = 0.5)
if r.status_code == 200:
return
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException):
pass
time.sleep(0.05)
raise RuntimeError(f"uvicorn did not become ready within {timeout}s")
def stop(self):
if self._server is not None:
self._server.should_exit = True
if self._thread is not None:
self._thread.join(timeout = 5.0)
def __enter__(self):
return self.start()
def __exit__(self, *exc):
self.stop()
def _build_app(backend, *, wrap_in_thread: bool):
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
if wrap_in_thread:
@app.get("/probe")
async def probe():
return {"audio_type": await asyncio.to_thread(backend.detect_audio_type)}
else:
@app.get("/probe")
async def probe():
return {"audio_type": backend.detect_audio_type()}
return app
def _drive_concurrent_probe_and_health(
base_url,
*,
n_health = 12,
gap = 0.05,
):
elapsed = -1.0
latencies: list[float] = []
def fire_probe():
nonlocal elapsed
t0 = time.perf_counter()
with httpx.Client(timeout = 30.0) as c:
r = c.get(f"{base_url}/probe")
assert r.status_code == 200
elapsed = time.perf_counter() - t0
def fire_health():
time.sleep(0.1)
with httpx.Client(timeout = 10.0) as c:
for _ in range(n_health):
t0 = time.perf_counter()
r = c.get(f"{base_url}/health")
latencies.append(time.perf_counter() - t0)
assert r.status_code == 200
time.sleep(gap)
with ThreadPoolExecutor(max_workers = 2) as pool:
f1 = pool.submit(fire_probe)
f2 = pool.submit(fire_health)
f1.result(60.0)
f2.result(60.0)
return max(latencies), elapsed, latencies
# (1) Behavioural canary
def test_buggy_route_blocks_event_loop():
"""Sync detect_audio_type call inside async route stalls /health."""
with FakeLlamaServer(tok_delay = 0.6, detok_delay = 0.6) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = False)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
max_lat, probe_t, _ = _drive_concurrent_probe_and_health(f"http://127.0.0.1:{uv.port}")
assert probe_t >= 0.5
assert max_lat >= 0.4, f"expected >=0.4s stall, got {max_lat:.3f}s"
def test_fixed_route_keeps_event_loop_responsive():
"""to_thread-wrapped call leaves the event loop free."""
with FakeLlamaServer(tok_delay = 0.6, detok_delay = 0.6) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
max_lat, probe_t, lats = _drive_concurrent_probe_and_health(
f"http://127.0.0.1:{uv.port}"
)
assert probe_t >= 0.5
assert max_lat < 0.25, f"expected <0.25s; got {max_lat:.3f}s (all: {lats})"
# (2) Functional equivalence -- sync == to_thread for each codec branch
@pytest.fixture
def shim_no_match():
"""Shim whose responses make detect_audio_type fall through every codec branch -> None."""
with FakeLlamaServer(
# detok strings don't start with "<custom_token_" so snac branch fails.
detok_map = {128258: "abc", 128259: "def"},
# 2-token responses make every `len(_tok(...)) == 1` codec check fail.
tok_response_map = {
"<|AUDIO|>": [0, 1],
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1],
"<audio_soft_token>": [0, 1],
"<|audio|>": [0, 1],
"<|bicodec_semantic_0|>": [0, 1],
"<|bicodec_global_0|>": [0, 1],
"<|c1_0|>": [0, 1],
"<|c2_0|>": [0, 1],
},
) as srv:
yield srv
def test_functional_equivalence_no_match(shim_no_match):
backend = _make_backend(shim_no_match.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == threaded == None # noqa: E711
def test_functional_equivalence_snac_match():
# snac: both _detok(128258) and _detok(128259) start with "<custom_token_".
with FakeLlamaServer(
detok_map = {128258: "<custom_token_99>", 128259: "<custom_token_98>"}
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "snac"
assert sync_result == threaded
def test_functional_equivalence_csm_match():
# csm: snac fails, then both <|AUDIO|> and <|audio_eos|> are 1 token.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {"<|AUDIO|>": [0], "<|audio_eos|>": [0]},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "csm"
assert sync_result == threaded
def test_functional_equivalence_whisper_match():
# whisper: snac/csm fail, then <|startoftranscript|> is 1 token.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {
"<|AUDIO|>": [0, 1], # csm fails (>1 token)
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0],
},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "whisper"
assert sync_result == threaded
def test_functional_equivalence_audio_vlm_match():
# audio_vlm: snac/csm/whisper fail, then the Gemma 4 <|audio|> arm (#6000)
# tokenises to 1 token while <audio_soft_token> stays 2 to isolate it.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {
"<|AUDIO|>": [0, 1], # csm fails (>1 token)
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1], # whisper fails
"<audio_soft_token>": [0, 1], # Gemma 3n arm fails ...
"<|audio|>": [0], # ... Gemma 4 arm matches (#6000)
},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "audio_vlm"
assert sync_result == threaded
def test_functional_equivalence_bicodec_match():
# bicodec: all prior branches fail, then bicodec_semantic_0/global_0 are 1 token.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {
"<|AUDIO|>": [0, 1],
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1],
"<audio_soft_token>": [0, 1],
"<|audio|>": [0, 1],
"<|bicodec_semantic_0|>": [0],
"<|bicodec_global_0|>": [0],
},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "bicodec"
assert sync_result == threaded
# (3) Failure modes
def test_shim_returns_500_on_tokenize_returns_none():
"""Non-200 responses fall through to None on both sync and threaded paths."""
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_status = 500,
) as srv:
backend = _make_backend(srv.port)
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_shim_returns_malformed_json_returns_none():
"""Outer try/except catches r.json() failures."""
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_body = b"{this is not json",
) as srv:
backend = _make_backend(srv.port)
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_shim_connection_reset_returns_none():
"""Mid-response connection drop (RemoteProtocolError / ReadError) is caught."""
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_reset = True,
) as srv:
backend = _make_backend(srv.port)
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_unreachable_port_returns_none():
"""ConnectError on a dead port is swallowed -> None."""
backend = _make_backend(_free_port()) # nothing listening
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_backend_not_loaded_short_circuits():
"""is_loaded=False short-circuits to None with no network I/O (sub-ms both paths)."""
backend = _make_backend(_free_port(), loaded = False)
t0 = time.perf_counter()
sync = backend.detect_audio_type()
sync_t = time.perf_counter() - t0
t0 = time.perf_counter()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
threaded_t = time.perf_counter() - t0
assert sync is threaded is None
assert sync_t < 0.05
assert threaded_t < 0.05
# (4) Stress / concurrency
def test_50_concurrent_probes_complete_without_deadlock():
"""50 parallel /probe calls must not deadlock or serialise."""
with FakeLlamaServer(tok_delay = 0.05, detok_delay = 0.05) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers = 50) as pool:
futs = [
pool.submit(
lambda: httpx.get(f"http://127.0.0.1:{uv.port}/probe", timeout = 30.0)
)
for _ in range(50)
]
results = [f.result(60.0) for f in futs]
elapsed = time.perf_counter() - t0
assert all(r.status_code == 200 for r in results)
# Generous bound absorbs CI jitter but still catches serialisation (~20s).
assert (
elapsed < 15.0
), f"50 concurrent probes took {elapsed:.1f}s; threadpool may be serialising"
def test_100_concurrent_healths_during_slow_probe_all_responsive():
"""100 /health across 8 threads during a slow /probe: latency stays bounded with the fix."""
with FakeLlamaServer(tok_delay = 0.4, detok_delay = 0.4) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
base = f"http://127.0.0.1:{uv.port}"
def probe():
with httpx.Client(timeout = 30.0) as c:
return c.get(f"{base}/probe").status_code
def health_burst(n):
lats = []
with httpx.Client(timeout = 10.0) as c:
for _ in range(n):
t0 = time.perf_counter()
assert c.get(f"{base}/health").status_code == 200
lats.append(time.perf_counter() - t0)
return lats
with ThreadPoolExecutor(max_workers = 9) as pool:
probe_f = pool.submit(probe)
time.sleep(0.05) # let probe enter detect_audio_type first
health_fs = [pool.submit(health_burst, 13) for _ in range(8)]
assert probe_f.result(60.0) == 200
latencies = [x for f in health_fs for x in f.result(60.0)]
assert len(latencies) == 104
max_lat = max(latencies)
assert max_lat < 0.35, f"100-burst max latency {max_lat:.3f}s exceeds 350 ms"
# (5) Drift / regression guards on the production source
def test_load_model_caches_audio_type_inside_serial_load_lock():
"""Audio-type detection must run inside load_model under _serial_load_lock,
else a concurrent /load can replace the backend mid-probe (review on #5669)."""
f = _REPO_ROOT / "studio" / "backend" / "core" / "inference" / "llama_cpp.py"
text = f.read_text(encoding = "utf-8")
assert (
"with self._serial_load_lock" in text
), "LlamaCppBackend.load_model must hold self._serial_load_lock"
# Either call shape satisfies the guard; _detect_audio_type_strict was a
# follow-up to distinguish definitive non-audio from transient probe failure.
assert (
"self._audio_type = self.detect_audio_type()" in text
or "detected = self.detect_audio_type()" in text
or "detected = self._detect_audio_type_strict()" in text
), (
"LlamaCppBackend.load_model must call detect_audio_type / "
"_detect_audio_type_strict and cache the result on "
"self._audio_type (#5642 follow-up)."
)
def test_routes_inference_reads_cached_audio_type_not_calls_detect():
"""routes/inference.py must read cached _audio_type/_is_audio, not call
detect_audio_type / init_audio_codec directly (both moved into load_model)."""
f = _REPO_ROOT / "studio" / "backend" / "routes" / "inference.py"
text = f.read_text(encoding = "utf-8")
assert "llama_backend.detect_audio_type(" not in text, (
"routes/inference.py should not call detect_audio_type directly; "
"load_model already cached it under the lock."
)
assert "llama_backend.init_audio_codec(" not in text, (
"routes/inference.py should not call init_audio_codec directly; "
"load_model already invoked it under the lock when audio_type was a TTS codec."
)
# Route must read the cached values.
assert "llama_backend._audio_type" in text
assert "llama_backend._is_audio" in text
def test_no_other_async_route_calls_detect_audio_type_unwrapped():
"""No routes/*.py may call llama_backend.detect_audio_type() in an async fn;
that reintroduces the sync bug and the load race the lock fix closes."""
routes_dir = _REPO_ROOT / "studio" / "backend" / "routes"
offenders = []
# Matches both llama_backend. and self. prefixes; the model_config free
# function helper is excluded below.
pattern = re.compile(r"\b\w+\.detect_audio_type\s*\(")
for path in routes_dir.rglob("*.py"):
for i, line in enumerate(path.read_text(encoding = "utf-8").splitlines(), start = 1):
m = pattern.search(line)
if not m:
continue
# Only the LlamaCppBackend instance call is an offender.
if "llama_backend.detect_audio_type" not in line:
continue
if "asyncio.to_thread" in line:
# Wrapped sync call is acceptable (not preferred); surface in PR.
continue
offenders.append(f"{path.relative_to(_REPO_ROOT)}:{i}: {line.strip()}")
assert not offenders, (
"routes/*.py contains llama_backend.detect_audio_type() calls; "
"the call should live inside load_model now: " + "; ".join(offenders)
)
# (6) Timing budgets
def test_load_response_under_2s_with_fast_shim():
"""Regression budget: fast shim must complete /probe in <2 s."""
with FakeLlamaServer(tok_delay = 0.0, detok_delay = 0.0) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
t0 = time.perf_counter()
with httpx.Client(timeout = 5.0) as c:
assert c.get(f"http://127.0.0.1:{uv.port}/probe").status_code == 200
elapsed = time.perf_counter() - t0
assert elapsed < 2.0
def test_repeated_loads_bounded_total_time():
"""Five sequential /probe calls finish under 10 s, guarding against per-call leaks."""
with FakeLlamaServer(tok_delay = 0.05, detok_delay = 0.05) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
t0 = time.perf_counter()
with httpx.Client(timeout = 5.0) as c:
for _ in range(5):
assert c.get(f"http://127.0.0.1:{uv.port}/probe").status_code == 200
elapsed = time.perf_counter() - t0
assert elapsed < 10.0
# (7) Browser-compatibility surface
def test_response_is_valid_browser_parseable_json():
"""The fix must not change the response shape a browser sees (valid JSON, expected keys)."""
import json as _json
with FakeLlamaServer(tok_delay = 0.0, detok_delay = 0.0) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
with httpx.Client(timeout = 5.0) as c:
r = c.get(f"http://127.0.0.1:{uv.port}/probe")
assert r.status_code == 200
assert r.headers["content-type"].startswith("application/json")
parsed = _json.loads(r.text)
assert "audio_type" in parsed
# No NaN / Infinity that would break browser parsers.
assert _json.dumps(parsed)
def test_response_shape_matches_pre_fix_for_no_match():
"""Sync and threaded paths return identical bodies for the no-match scenario."""
import json as _json
with FakeLlamaServer(
detok_map = {128258: "abc", 128259: "def"},
tok_response_map = {
"<|AUDIO|>": [0, 1],
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1],
"<audio_soft_token>": [0, 1],
"<|audio|>": [0, 1],
"<|bicodec_semantic_0|>": [0, 1],
"<|bicodec_global_0|>": [0, 1],
"<|c1_0|>": [0, 1],
"<|c2_0|>": [0, 1],
},
) as shim:
backend = _make_backend(shim.port)
# sync (pre-fix) then to_thread (post-fix).
for wrap in (False, True):
app = _build_app(backend, wrap_in_thread = wrap)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
with httpx.Client(timeout = 30.0) as c:
r = c.get(f"http://127.0.0.1:{uv.port}/probe")
assert r.status_code == 200
body = _json.loads(r.text)
assert body == {"audio_type": None}
# (8) Cancellation
def test_client_disconnect_during_probe_does_not_crash_server():
"""A client disconnect mid-probe must not crash the server; /health still responds."""
with FakeLlamaServer(tok_delay = 0.5, detok_delay = 0.5) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
base = f"http://127.0.0.1:{uv.port}"
# Short timeout simulates a client that gave up mid-probe.
with pytest.raises(httpx.TimeoutException):
with httpx.Client(timeout = 0.2) as c:
c.get(f"{base}/probe")
with httpx.Client(timeout = 5.0) as c:
r = c.get(f"{base}/health")
assert r.status_code == 200