"""Tests for the endpoint chat app.
Run: pytest -q (the required environment is filled in below)
"""
import json
import os
import pytest
import requests
# Assigned outright so real values exported in the shell cannot leak in.
os.environ.update(
ENDPOINT_URL="http://endpoint.test/",
QWEN_ENDPOINT_URL="http://qwen.endpoint.test/",
DEEPSEEK_ENDPOINT_URL="http://deepseek.endpoint.test/",
HF_TOKEN="test-token",
QWEN_HF_TOKEN="qwen-org-token",
)
os.environ.pop("DEEPSEEK_HF_TOKEN", None)
os.environ.setdefault("LOGIN_PASSWORD", "s3cret")
import app
def drain(generator):
"""Consume a streaming handler, returning (all yields, final yield)."""
frames = list(generator)
return frames, frames[-1]
def histories(frames):
return [frame[0] for frame in frames]
def textbox(frame):
"""The value the textbox update carries."""
update = frame[1]
return update["value"] if isinstance(update, dict) else update
def locked(frame):
"""True when the frame locks the input controls (a request is in flight)."""
msg_update, send_update, stop_update = frame[1:]
return (
msg_update["interactive"] is False
and send_update["interactive"] is False
and stop_update["interactive"] is True
)
def drain_failing(generator, expected):
"""Consume a streaming handler that fails partway; returns frames seen."""
frames = []
with pytest.raises(expected):
for frame in generator:
frames.append(frame) # noqa: PERF402 - loop can raise mid-iteration
return frames
def fake_stream(*chunks):
def stream(messages, model_name, turn="-"):
yield from chunks
return stream
def test_build_api_messages_keeps_order_and_appends_new_turn():
history = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello"},
]
assert app.build_api_messages(history, "how are you?") == [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello"},
{"role": "user", "content": "how are you?"},
]
def test_build_api_messages_flattens_gradio_typed_content():
"""Gradio hands handlers typed content parts, not plain strings."""
import gradio as gr
chatbot = gr.Chatbot()
history = chatbot.preprocess(
chatbot.postprocess(
[
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello"},
]
)
)
assert history[0]["content"] != "hi", "test no longer exercises typed parts"
assert app.build_api_messages(history, "next") == [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello"},
{"role": "user", "content": "next"},
]
def test_build_api_messages_drops_non_text_and_malformed_entries():
history = [
{"role": "user", "content": [{"type": "file", "file": {"path": "a.png"}}]},
{"role": "system", "content": "ignored"},
{"role": "user"},
None,
{"role": "assistant", "content": "kept"},
]
assert app.build_api_messages(history, "next") == [
{"role": "assistant", "content": "kept"},
{"role": "user", "content": "next"},
]
def test_message_text_handles_mixed_and_unknown_content():
assert app.message_text("plain") == "plain"
assert app.message_text([{"type": "text", "text": "a"}, {"type": "file"}]) == "a"
assert (
app.message_text([{"type": "text", "text": "a"}, {"type": "text", "text": "b"}])
== "ab"
)
assert app.message_text([{"type": "text", "text": None}]) == ""
assert app.message_text(None) == ""
assert app.message_text(42) == ""
def test_respond_streams_partial_replies_then_final(monkeypatch):
monkeypatch.setattr(app, "stream_completion", fake_stream("Hel", "lo ", "there"))
frames, final = drain(app.respond("ping", [], app.DEFAULT_MODEL))
history = final[0]
# Assistant text grows monotonically across frames.
assert [h[-1]["content"] for h in histories(frames)] == [
app.WAITING_TEXT,
"Hel",
"Hello ",
"Hello there",
"Hello there",
]
assert history == [
{"role": "user", "content": "ping"},
{"role": "assistant", "content": "Hello there"},
]
assert textbox(final) == ""
def test_respond_shows_pending_spinner_until_first_token(monkeypatch):
monkeypatch.setattr(app, "stream_completion", fake_stream("first token"))
frames, final = drain(app.respond("ping", [], app.DEFAULT_MODEL))
# Frame 1: user turn plus a pending placeholder that renders as a spinner.
placeholder = frames[0][0][-1]
assert placeholder["metadata"]["status"] == "pending"
assert placeholder["metadata"]["title"]
# Regression: the Chatbot with reasoning_tags silently dropped the empty
# placeholder, so a sleeping endpoint showed no feedback at all.
import gradio as gr
chatbot = next(b for b in app.demo.blocks.values() if isinstance(b, gr.Chatbot))
rendered = chatbot.postprocess(frames[0][0]).root
assert [m.metadata.get("status") if m.metadata else None for m in rendered] == [
None,
"pending",
]
# The spinner is gone the moment real text exists, and never comes back.
for history in histories(frames[1:]):
assert "metadata" not in history[-1], "spinner outlived the first token"
assert "metadata" not in final[0][-1]
def test_respond_locks_controls_only_while_in_flight(monkeypatch):
monkeypatch.setattr(app, "stream_completion", fake_stream("a", "b"))
frames, final = drain(app.respond("ping", [], app.DEFAULT_MODEL))
assert [locked(f) for f in frames] == [True, True, True, False]
assert locked(final) is False
def test_respond_unlocks_controls_when_message_is_blank(monkeypatch):
monkeypatch.setattr(app, "stream_completion", fake_stream("unused"))
_frames, final = drain(app.respond("", [], app.DEFAULT_MODEL))
assert locked(final) is False
def test_respond_rejects_an_empty_reply(monkeypatch):
import gradio as gr
monkeypatch.setattr(app, "stream_completion", fake_stream())
frames = drain_failing(app.respond("ping", [], app.DEFAULT_MODEL), gr.Error)
# No stuck spinner, no empty bubble, and the controls are released.
assert frames[-1][0] == []
assert locked(frames[-1]) is False
def test_respond_history_survives_gradio_format_check(monkeypatch):
"""Regression: tuple history raised 'Data incompatible with messages format'."""
import gradio as gr
monkeypatch.setattr(app, "stream_completion", fake_stream("pong"))
frames, _ = drain(app.respond("ping", [], app.DEFAULT_MODEL))
for history in histories(frames):
gr.Chatbot()._check_format(history)
def test_respond_sends_history_without_the_placeholder_turn(monkeypatch):
sent = {}
def stream(messages, model_name, turn="-"):
sent["messages"] = messages
yield "ok"
monkeypatch.setattr(app, "stream_completion", stream)
drain(app.respond("second", [{"role": "user", "content": "first"}], None))
assert sent["messages"] == [
{"role": "user", "content": "first"},
{"role": "user", "content": "second"},
]
def test_respond_ignores_blank_message(monkeypatch):
def fail(*args, **kwargs):
raise AssertionError("endpoint should not be called for a blank message")
monkeypatch.setattr(app, "stream_completion", fail)
_frames, final = drain(
app.respond(" ", [{"role": "user", "content": "hi"}], None)
)
assert final[0] == [{"role": "user", "content": "hi"}]
def test_respond_does_not_mutate_caller_history(monkeypatch):
monkeypatch.setattr(app, "stream_completion", fake_stream("pong"))
history = []
drain(app.respond("ping", history, app.DEFAULT_MODEL))
assert history == []
@pytest.mark.parametrize(
"error",
[
requests.exceptions.Timeout(),
requests.exceptions.ConnectionError("refused"),
ValueError("not json"),
],
)
def test_respond_surfaces_immediate_failures_and_restores_input(monkeypatch, error):
import gradio as gr
def stream(messages, model_name, turn="-"):
raise error
yield # pragma: no cover - makes this a generator
monkeypatch.setattr(app, "stream_completion", stream)
frames = drain_failing(app.respond("ping", [], app.DEFAULT_MODEL), gr.Error)
# Nothing streamed, so the whole failed turn is rolled back and the user's
# text is handed back to the input box rather than duplicated in the chat.
assert frames[-1][0] == []
assert textbox(frames[-1]) == "ping"
assert locked(frames[-1]) is False
def test_respond_keeps_partial_reply_when_stream_breaks(monkeypatch):
import gradio as gr
def stream(messages, model_name, turn="-"):
yield "half a "
raise requests.exceptions.ChunkedEncodingError("dropped")
monkeypatch.setattr(app, "stream_completion", stream)
frames = drain_failing(app.respond("ping", [], app.DEFAULT_MODEL), gr.Error)
assert frames[-1][0] == [
{"role": "user", "content": "ping"},
{"role": "assistant", "content": "half a "},
]
# A kept partial reply means the box is not refilled with the same text.
assert textbox(frames[-1]) == ""
assert locked(frames[-1]) is False
def test_respond_reraises_unexpected_errors(monkeypatch):
def stream(messages, model_name, turn="-"):
raise RuntimeError("boom")
yield # pragma: no cover
monkeypatch.setattr(app, "stream_completion", stream)
with pytest.raises(RuntimeError, match="boom"):
drain(app.respond("ping", [], app.DEFAULT_MODEL))
def test_verify_login_rejects_wrong_and_empty_password():
assert app.verify_login("user", "s3cret") is True
assert app.verify_login("user", "wrong") is False
assert app.verify_login("user", "") is False
assert app.verify_login("user", None) is False
def test_missing_required_env_fails_with_named_message(monkeypatch):
monkeypatch.delenv("ENDPOINT_URL", raising=False)
with pytest.raises(RuntimeError, match="ENDPOINT_URL"):
app._required_env("ENDPOINT_URL")
class FakeStreamResponse:
"""Stand-in for the streaming `requests` response."""
def __init__(self, lines, status_code=200):
self._lines = lines
self.status_code = status_code
self.closed = False
def __enter__(self):
return self
def __exit__(self, *exc_info):
self.closed = True
def raise_for_status(self):
pass
def iter_lines(self, chunk_size=512, decode_unicode=False):
self.iter_kwargs = {"chunk_size": chunk_size, "decode_unicode": decode_unicode}
yield from self._lines
def sse(*payloads):
return [f"data: {p}" for p in payloads]
def chunk(content):
return json.dumps({"choices": [{"delta": {"content": content}}]})
def test_each_model_posts_to_its_own_endpoint(monkeypatch):
captured = {}
def fake_post(url, **kwargs):
captured.update(url=url, **kwargs)
return FakeStreamResponse(sse(chunk("hi"), "[DONE]"))
monkeypatch.setattr(app.requests, "post", fake_post)
expected = {
app.GEMMA_MODEL: "http://endpoint.test",
app.QWEN_MODEL: "http://qwen.endpoint.test",
app.DEEPSEEK_MODEL: "http://deepseek.endpoint.test",
}
for model, url in expected.items():
list(app.stream_completion([{"role": "user", "content": "hi"}], model))
assert captured["url"] == f"{url}/v1/chat/completions"
assert captured["json"]["model"] == model
assert app.AVAILABLE_MODELS == [
app.GEMMA_MODEL,
app.QWEN_MODEL,
app.DEEPSEEK_MODEL,
]
def test_each_model_sends_its_own_token(monkeypatch):
"""Endpoints in different orgs need tokens with access to that org."""
sent = {}
def fake_post(url, **kwargs):
sent[url] = kwargs["headers"]["Authorization"]
return FakeStreamResponse(sse(chunk("hi"), "[DONE]"))
monkeypatch.setattr(app.requests, "post", fake_post)
for model in app.AVAILABLE_MODELS:
list(app.stream_completion([], model))
assert sent == {
"http://endpoint.test/v1/chat/completions": "Bearer test-token",
"http://qwen.endpoint.test/v1/chat/completions": "Bearer qwen-org-token",
# No DEEPSEEK_HF_TOKEN, so it falls back to HF_TOKEN.
"http://deepseek.endpoint.test/v1/chat/completions": "Bearer test-token",
}
assert app.MODELS[app.QWEN_MODEL].token_env == "QWEN_HF_TOKEN"
assert app.MODELS[app.DEEPSEEK_MODEL].token_env == "HF_TOKEN"
@pytest.mark.parametrize("status", [401, 403])
def test_rejected_token_names_the_secret_to_fix(monkeypatch, status):
import gradio as gr
monkeypatch.setattr(
app.requests, "post", lambda url, **kw: FakeStreamResponse([], status)
)
with pytest.raises(gr.Error, match="QWEN_HF_TOKEN") as error:
list(app.stream_completion([], app.QWEN_MODEL))
assert "qwen-org-token" not in str(error.value)
def test_stream_completion_parses_sse_chunks(monkeypatch):
lines = [
": keep-alive",
"",
*sse(chunk("Hel"), chunk("lo")),
"",
# Final chunk carries a finish_reason and no content.
json.dumps({"choices": [{"delta": {}, "finish_reason": "stop"}]}).join(
["data: ", ""]
),
"data: [DONE]",
"data: {bad json that must never be reached}",
]
response = FakeStreamResponse(lines)
captured = {}
def fake_post(url, **kwargs):
captured.update(url=url, **kwargs)
return response
monkeypatch.setattr(app.requests, "post", fake_post)
assert list(app.stream_completion([{"role": "user", "content": "hi"}], "m")) == [
"Hel",
"lo",
]
assert captured["url"] == "http://endpoint.test/v1/chat/completions"
assert captured["json"]["stream"] is True
assert captured["stream"] is True
assert captured["headers"]["Authorization"] == "Bearer test-token"
assert response.closed
def reasoning_chunk(text):
return json.dumps({"choices": [{"delta": {"reasoning_content": text}}]})
def test_stream_completion_wraps_reasoning_in_think_tags(monkeypatch):
"""llama.cpp streams thinking as reasoning_content; the Chatbot needs tags."""
lines = sse(reasoning_chunk("7 x "), reasoning_chunk("143"), chunk("No."), "[DONE]")
monkeypatch.setattr(
app.requests, "post", lambda url, **kwargs: FakeStreamResponse(lines)
)
assert "".join(app.stream_completion([], "m")) == "7 x 143No."
def test_stream_completion_closes_reasoning_cut_off_mid_thought(monkeypatch):
lines = sse(reasoning_chunk("still thinking"), "[DONE]")
monkeypatch.setattr(
app.requests, "post", lambda url, **kwargs: FakeStreamResponse(lines)
)
assert "".join(app.stream_completion([], "m")) == "still thinking"
def test_reasoning_is_not_sent_back_to_the_model():
"""The Chatbot splits reasoning into a titled thought, which is dropped."""
import gradio as gr
chatbot = gr.Chatbot(reasoning_tags=[(app.THINK_OPEN, app.THINK_CLOSE)])
history = chatbot.preprocess(
chatbot.postprocess(
[
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hmmhello"},
]
)
)
assert app.build_api_messages(history, "next") == [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello"},
{"role": "user", "content": "next"},
]
def test_stop_replaces_send_while_a_reply_streams():
_msg, send, stop = app.busy_controls()
assert send["visible"] is False and stop["visible"] is True
_msg, send, stop = app.idle_controls()
assert send["visible"] is True and stop["visible"] is False
def test_new_chat_empties_history_and_unlocks():
history, *controls = app.new_chat()
assert history == []
assert controls[0]["interactive"] is True
def test_every_model_has_a_display_label():
assert set(app.MODEL_LABELS) == set(app.AVAILABLE_MODELS)
def test_stream_completion_tolerates_chunks_without_choices(monkeypatch):
lines = sse(json.dumps({"choices": []}), chunk("ok"), "[DONE]")
monkeypatch.setattr(
app.requests, "post", lambda url, **kwargs: FakeStreamResponse(lines)
)
assert list(app.stream_completion([], "m")) == ["ok"]
def test_stream_completion_raises_on_malformed_chunk(monkeypatch):
lines = sse(chunk("ok"), "{not json")
monkeypatch.setattr(
app.requests, "post", lambda url, **kwargs: FakeStreamResponse(lines)
)
with pytest.raises(ValueError):
list(app.stream_completion([], "m"))
def test_module_exposes_demo_for_spaces_hot_reload():
"""Spaces' reloader does getattr(__main__, "demo") after every reload."""
import gradio as gr
assert isinstance(app.demo, gr.Blocks)
def test_cancel_stream_clears_spinner_and_unlocks(monkeypatch):
"""Stop cancels the generator mid-flight, so cleanup runs in its own event."""
monkeypatch.setattr(app, "stream_completion", fake_stream("half"))
frames, _final = drain(app.respond("ping", [], app.DEFAULT_MODEL))
mid_flight = frames[0][0] # still showing the pending spinner
history, *controls = app.cancel_stream(mid_flight)
assert history == [{"role": "user", "content": "ping"}]
assert all(update["interactive"] is not False for update in controls[:2])
def test_normalize_history_drops_thought_placeholders():
history = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "", "metadata": app.WAITING_METADATA},
{"role": "assistant", "content": "kept", "metadata": {}},
]
assert app.normalize_history(history) == [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "kept"},
]
def test_stream_completion_reads_incrementally(monkeypatch):
"""Regression: the default 512-byte reads deliver the whole reply at once."""
response = FakeStreamResponse(sse(chunk("hi"), "[DONE]"))
monkeypatch.setattr(app.requests, "post", lambda url, **kwargs: response)
list(app.stream_completion([], "m"))
assert response.iter_kwargs["chunk_size"] == 1
assert response.iter_kwargs["decode_unicode"] is True
def test_successful_turn_is_logged(monkeypatch, caplog):
monkeypatch.setattr(app, "stream_completion", fake_stream("pong"))
with caplog.at_level("INFO", logger="gemma-endpoint"):
drain(app.respond("ping", [], app.DEFAULT_MODEL))
messages = [r.getMessage() for r in caplog.records]
assert any("start model=" in m for m in messages)
assert any("complete reply_chars=4" in m for m in messages)
def test_failed_turn_is_logged_at_error(monkeypatch, caplog):
import gradio as gr
def stream(messages, model_name, turn="-"):
raise requests.exceptions.ConnectionError("refused")
yield # pragma: no cover
monkeypatch.setattr(app, "stream_completion", stream)
with caplog.at_level("INFO", logger="gemma-endpoint"):
drain_failing(app.respond("ping", [], app.DEFAULT_MODEL), gr.Error)
errors = [r.getMessage() for r in caplog.records if r.levelname == "ERROR"]
assert any("failed after" in m and "refused" in m for m in errors)
def test_unexpected_error_logs_a_traceback(monkeypatch, caplog):
def stream(messages, model_name, turn="-"):
raise RuntimeError("boom")
yield # pragma: no cover
monkeypatch.setattr(app, "stream_completion", stream)
with caplog.at_level("INFO", logger="gemma-endpoint"), pytest.raises(RuntimeError):
drain(app.respond("ping", [], app.DEFAULT_MODEL))
assert any(r.exc_info for r in caplog.records), "no traceback was logged"
def test_turn_ids_correlate_lines_of_one_turn(monkeypatch, caplog):
def stream(messages, model_name, turn="-"):
app.logger.info("turn=%s inner", turn)
yield "ok"
monkeypatch.setattr(app, "stream_completion", stream)
with caplog.at_level("INFO", logger="gemma-endpoint"):
drain(app.respond("one", [], app.DEFAULT_MODEL))
first = {
m.split("turn=")[1].split()[0]
for m in (r.getMessage() for r in caplog.records)
if "turn=" in m
}
caplog.clear()
drain(app.respond("two", [], app.DEFAULT_MODEL))
second = {
m.split("turn=")[1].split()[0]
for m in (r.getMessage() for r in caplog.records)
if "turn=" in m
}
assert len(first) == 1 and len(second) == 1, "one turn must use one id"
assert first != second, "consecutive turns must not share an id"
def test_rejected_login_is_logged_without_the_password(caplog):
with caplog.at_level("INFO", logger="gemma-endpoint"):
assert app.verify_login("intruder", "hunter2") is False
record = caplog.records[-1]
assert record.levelname == "WARNING"
assert "REJECTED" in record.getMessage()
assert "hunter2" not in record.getMessage()
assert "s3cret" not in record.getMessage()
def test_logs_never_contain_the_hf_token(monkeypatch, caplog):
"""The token is only ever an Authorization header, never a log field."""
response = FakeStreamResponse(sse(chunk("hi"), "[DONE]"))
monkeypatch.setattr(app.requests, "post", lambda url, **kwargs: response)
with caplog.at_level("DEBUG", logger="gemma-endpoint"):
for model in app.AVAILABLE_MODELS:
list(
app.stream_completion(
[{"role": "user", "content": "hi"}], model, turn=1
)
)
assert caplog.records, "expected the request to be logged"
tokens = {endpoint.token for endpoint in app.MODELS.values()}
for record in caplog.records:
assert not any(token in record.getMessage() for token in tokens)
assert "Authorization" not in record.getMessage()
def test_log_level_applies_to_this_app_only():
"""LOG_LEVEL must not switch on httpx/urllib3 chatter along with ours."""
import logging
assert app.logger.level == logging.INFO
assert logging.getLogger().level == logging.WARNING
assert app.logger.isEnabledFor(logging.INFO)
assert not logging.getLogger("httpx").isEnabledFor(logging.INFO)
@pytest.fixture
def no_sleep(monkeypatch):
"""Run retry loops at full speed, with a clock that still advances."""
clock = {"now": 0.0}
monkeypatch.setattr(app.time, "monotonic", lambda: clock["now"])
monkeypatch.setattr(
app.time, "sleep", lambda seconds: clock.update(now=clock["now"] + seconds)
)
return clock
@pytest.mark.parametrize("status", [502, 503, 504])
def test_stream_completion_flags_a_cold_endpoint(monkeypatch, status):
monkeypatch.setattr(
app.requests, "post", lambda url, **kw: FakeStreamResponse([], status)
)
with pytest.raises(app.EndpointWaking):
list(app.stream_completion([], "m"))
def test_stream_completion_still_raises_for_real_errors(monkeypatch):
response = FakeStreamResponse([], 404)
def raise_for_status():
raise requests.exceptions.HTTPError("404", response=None)
response.raise_for_status = raise_for_status
monkeypatch.setattr(app.requests, "post", lambda url, **kw: response)
with pytest.raises(requests.exceptions.HTTPError):
list(app.stream_completion([], "m"))
def test_stream_turn_waits_out_a_cold_endpoint(monkeypatch, no_sleep):
attempts = {"n": 0}
def stream(messages, model_name, turn="-"):
attempts["n"] += 1
if attempts["n"] < 3:
raise app.EndpointWaking(503)
yield # pragma: no cover
yield "awake"
monkeypatch.setattr(app, "stream_completion", stream)
events = list(app.stream_turn([], "m", turn=1))
assert attempts["n"] == 3, "should retry until the endpoint answers"
assert ("token", "awake") in events
waking = [value for kind, value in events if kind == "waking"]
assert waking, "no waking updates were emitted"
assert waking == sorted(waking), "the waited counter must not go backwards"
def test_stream_turn_gives_up_after_the_wake_timeout(monkeypatch, no_sleep):
import gradio as gr
def always_cold(messages, model_name, turn="-"):
raise app.EndpointWaking(503)
yield # pragma: no cover
monkeypatch.setattr(app, "stream_completion", always_cold)
with pytest.raises(gr.Error, match="did not come up"):
for _ in app.stream_turn([], "m", turn=1):
pass
assert no_sleep["now"] >= app.WAKE_TIMEOUT_SECONDS
def test_respond_shows_a_waking_message_then_the_reply(monkeypatch, no_sleep):
attempts = {"n": 0}
def stream(messages, model_name, turn="-"):
attempts["n"] += 1
if attempts["n"] == 1:
raise app.EndpointWaking(503)
yield # pragma: no cover
yield "hello"
monkeypatch.setattr(app, "stream_completion", stream)
frames, final = drain(app.respond("ping", [], app.DEFAULT_MODEL))
titles = [
(h[-1].get("metadata") or {}).get("title") for h in histories(frames) if h
]
assert any(t and "Waking up the endpoint" in t for t in titles)
assert all(locked(f) for f in frames[:-1]), "controls stay locked while waking"
assert final[0][-1] == {"role": "assistant", "content": "hello"}
def test_respond_reports_a_cold_endpoint_that_never_wakes(monkeypatch, no_sleep):
import gradio as gr
def always_cold(messages, model_name, turn="-"):
raise app.EndpointWaking(503)
yield # pragma: no cover
monkeypatch.setattr(app, "stream_completion", always_cold)
frames = drain_failing(app.respond("ping", [], app.DEFAULT_MODEL), gr.Error)
assert frames[-1][0] == [], "the failed turn is rolled back"
assert textbox(frames[-1]) == "ping"
assert locked(frames[-1]) is False
class Utf8SseServer:
"""A real HTTP server streaming raw UTF-8 SSE, as vLLM/llama.cpp do.
`FakeStreamResponse` hands back `str` lines that are already decoded, so it
cannot catch a decoding bug. Only a real socket exercises the path where
`requests` has to pick a charset.
"""
def __init__(self, content_type):
import http.server
import threading
body = "".join(
f"data: {json.dumps({'choices': [{'delta': {'content': token}}]}, ensure_ascii=False)}\n\n"
for token in ("こんにちは", "、", "日本語", "です", "。")
)
raw = (body + "data: [DONE]\n\n").encode("utf-8")
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
self.rfile.read(int(self.headers.get("Content-Length") or 0))
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
# Split on a 7-byte boundary so multi-byte characters land
# across two chunks, as they do on a real connection.
for start in range(0, len(raw), 7):
piece = raw[start : start + 7]
self.wfile.write(b"%x\r\n%s\r\n" % (len(piece), piece))
self.wfile.flush()
self.wfile.write(b"0\r\n\r\n")
def log_message(self, *args):
pass
self._server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=self._server.serve_forever, daemon=True).start()
@property
def url(self):
return f"http://127.0.0.1:{self._server.server_port}"
def close(self):
self._server.shutdown()
@pytest.mark.parametrize(
"content_type",
["text/event-stream", "text/event-stream; charset=utf-8"],
)
def test_stream_completion_decodes_utf8_replies(monkeypatch, content_type):
"""Regression: a charset-less SSE response decoded Japanese as mojibake.
`requests` falls back to ISO-8859-1 for a text/* response with no charset,
so every non-ASCII reply came back garbled while English looked fine.
"""
server = Utf8SseServer(content_type)
monkeypatch.setattr(app, "ENDPOINT_URL", server.url)
try:
reply = "".join(app.stream_completion([], "m"))
finally:
server.close()
assert reply == "こんにちは、日本語です。"