Spaces:
Running on Zero
Running on Zero
| from unittest.mock import MagicMock, patch | |
| import pytest | |
| import requests | |
| from engine import openrouter_backend as ob | |
| # ββ prompt building / response parsing (pure functions) ββββββββββββββββββββ | |
| def test_build_batch_prompt_numbers_lines_in_order(): | |
| prompt = ob._build_batch_prompt("TEMPLATE", ["first", "second", "third"]) | |
| assert "1. first" in prompt | |
| assert "2. second" in prompt | |
| assert "3. third" in prompt | |
| assert prompt.startswith("TEMPLATE") | |
| def test_build_batch_prompt_includes_preceding_context_when_given(): | |
| prompt = ob._build_batch_prompt("TEMPLATE", ["text"], preceding=["prev1", "prev2"]) | |
| assert "prev1" in prompt | |
| assert "prev2" in prompt | |
| assert "translated immediately before" in prompt | |
| def test_build_batch_prompt_omits_context_block_when_no_preceding(): | |
| prompt = ob._build_batch_prompt("TEMPLATE", ["text"], preceding=None) | |
| assert "translated immediately before" not in prompt | |
| def test_parse_batch_response_happy_path(): | |
| response = "1. hello\n2. world\n3. foo bar" | |
| assert ob._parse_batch_response(response, 3) == ["hello", "world", "foo bar"] | |
| def test_parse_batch_response_wrong_count_returns_none(): | |
| response = "1. hello\n2. world" | |
| assert ob._parse_batch_response(response, 3) is None | |
| def test_parse_batch_response_ignores_non_numbered_lines(): | |
| response = "Sure, here you go:\n1. hello\n2. world\nThanks!" | |
| assert ob._parse_batch_response(response, 2) == ["hello", "world"] | |
| def test_parse_batch_response_empty_input(): | |
| assert ob._parse_batch_response("", 0) == [] | |
| assert ob._parse_batch_response("", 1) is None | |
| # ββ model listing / curation ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _model(id_, modality="text->text", output_modalities=None): | |
| return { | |
| "id": id_, | |
| "architecture": { | |
| "modality": modality, | |
| "output_modalities": output_modalities if output_modalities is not None else ["text"], | |
| }, | |
| } | |
| def test_is_text_model_accepts_text_to_text(): | |
| assert ob._is_text_model(_model("a/a", modality="text->text")) | |
| def test_is_text_model_accepts_multimodal_input_text_output(): | |
| assert ob._is_text_model(_model("a/a", modality="text+image->text")) | |
| def test_is_text_model_rejects_image_output(): | |
| m = _model("a/a", modality="text->image", output_modalities=["image"]) | |
| assert not ob._is_text_model(m) | |
| def test_list_model_choices_puts_curated_first_then_rest_alphabetically(): | |
| fetched = [ | |
| _model("zzz/last"), | |
| _model(ob.CURATED_MODELS[1]), | |
| _model("aaa/first"), | |
| _model(ob.CURATED_MODELS[0]), | |
| ] | |
| with patch.object(ob, "fetch_available_models", return_value=fetched): | |
| choices = ob.list_model_choices() | |
| assert choices == [ob.CURATED_MODELS[0], ob.CURATED_MODELS[1], "aaa/first", "zzz/last"] | |
| def test_list_model_choices_drops_curated_models_not_in_catalog(): | |
| fetched = [_model("aaa/first")] | |
| with patch.object(ob, "fetch_available_models", return_value=fetched): | |
| choices = ob.list_model_choices() | |
| assert choices == ["aaa/first"] | |
| for curated in ob.CURATED_MODELS: | |
| assert curated not in choices | |
| def test_list_model_choices_filters_out_non_text_models(): | |
| fetched = [ | |
| _model(ob.CURATED_MODELS[0]), | |
| _model("image/only", modality="text->image", output_modalities=["image"]), | |
| ] | |
| with patch.object(ob, "fetch_available_models", return_value=fetched): | |
| choices = ob.list_model_choices() | |
| assert "image/only" not in choices | |
| def test_list_model_choices_falls_back_to_curated_on_fetch_failure(): | |
| with patch.object(ob, "fetch_available_models", side_effect=requests.RequestException("network down")): | |
| choices = ob.list_model_choices() | |
| assert choices == list(ob.CURATED_MODELS) | |
| def test_fetch_available_models_hits_public_models_endpoint(): | |
| fake_resp = MagicMock() | |
| fake_resp.json.return_value = {"data": [{"id": "a/a"}]} | |
| with patch.object(ob.requests, "get", return_value=fake_resp) as mock_get: | |
| result = ob.fetch_available_models(timeout=5) | |
| mock_get.assert_called_once_with(f"{ob.BASE_URL}/models", timeout=5) | |
| fake_resp.raise_for_status.assert_called_once() | |
| assert result == [{"id": "a/a"}] | |
| # ββ _call_openrouter (network layer) ββββββββββββββββββββββββββββββββββββββββ | |
| def _http_response(status_code, json_body=None, text=""): | |
| resp = MagicMock() | |
| resp.status_code = status_code | |
| resp.text = text | |
| if json_body is not None: | |
| resp.json.return_value = json_body | |
| return resp | |
| def test_call_openrouter_success_returns_message_content(): | |
| ok = _http_response(200, {"choices": [{"message": {"content": "translated text"}}]}) | |
| with patch.object(ob.requests, "post", return_value=ok) as mock_post: | |
| result = ob._call_openrouter("key123", "some/model", "prompt text") | |
| assert result == "translated text" | |
| _, kwargs = mock_post.call_args | |
| assert kwargs["headers"]["Authorization"] == "Bearer key123" | |
| assert kwargs["json"]["models"][0] == "some/model" | |
| assert kwargs["json"]["messages"] == [{"role": "user", "content": "prompt text"}] | |
| def test_call_openrouter_includes_curated_models_as_serverside_fallback(): | |
| ok = _http_response(200, {"choices": [{"message": {"content": "x"}}]}) | |
| with patch.object(ob.requests, "post", return_value=ok) as mock_post: | |
| ob._call_openrouter("key", ob.CURATED_MODELS[2], "prompt") | |
| sent_models = mock_post.call_args.kwargs["json"]["models"] | |
| assert sent_models[0] == ob.CURATED_MODELS[2] | |
| assert set(sent_models) == set(ob.CURATED_MODELS) | |
| assert len(sent_models) == len(ob.CURATED_MODELS) # no duplicate of the primary model | |
| def test_call_openrouter_retries_on_retryable_status_then_succeeds(): | |
| bad = _http_response(429, text="rate limited") | |
| good = _http_response(200, {"choices": [{"message": {"content": "ok"}}]}) | |
| with patch.object(ob.requests, "post", side_effect=[bad, good]), patch.object(ob.time, "sleep"): | |
| result = ob._call_openrouter("key", "model", "prompt") | |
| assert result == "ok" | |
| def test_call_openrouter_raises_immediately_on_non_retryable_status(): | |
| unauthorized = _http_response(401, text="invalid api key") | |
| with patch.object(ob.requests, "post", return_value=unauthorized) as mock_post: | |
| with pytest.raises(RuntimeError, match="401"): | |
| ob._call_openrouter("bad-key", "model", "prompt") | |
| assert mock_post.call_count == 1 | |
| def test_call_openrouter_exhausts_retries_and_raises_last_error(): | |
| bad = _http_response(503, text="unavailable") | |
| with patch.object(ob.requests, "post", return_value=bad), patch.object(ob.time, "sleep"): | |
| with pytest.raises(RuntimeError, match="503"): | |
| ob._call_openrouter("key", "model", "prompt") | |
| def test_call_openrouter_retries_on_network_exception(): | |
| good = _http_response(200, {"choices": [{"message": {"content": "recovered"}}]}) | |
| with patch.object( | |
| ob.requests, "post", side_effect=[requests.ConnectionError("boom"), good] | |
| ), patch.object(ob.time, "sleep"): | |
| result = ob._call_openrouter("key", "model", "prompt") | |
| assert result == "recovered" | |
| # ββ translate_one / translate_batch βββββββββββββββββββββββββββββββββββββββββ | |
| def test_translate_one_strips_whitespace_from_response(): | |
| with patch.object(ob, "_call_openrouter", return_value=" translated \n"): | |
| result = ob.translate_one("source text", "key", "model", "template") | |
| assert result == "translated" | |
| def test_translate_batch_empty_texts_short_circuits_without_network(): | |
| with patch.object(ob, "_call_openrouter") as mock_call: | |
| result = ob.translate_batch([], "key", "model", "template") | |
| assert result == [] | |
| mock_call.assert_not_called() | |
| def test_translate_batch_happy_path(): | |
| with patch.object(ob, "_call_openrouter", return_value="1. one\n2. two"): | |
| result = ob.translate_batch(["a", "b"], "key", "model", "template") | |
| assert result == ["one", "two"] | |
| def test_translate_batch_returns_none_on_parse_mismatch(): | |
| with patch.object(ob, "_call_openrouter", return_value="1. only one line"): | |
| result = ob.translate_batch(["a", "b"], "key", "model", "template") | |
| assert result is None | |
| def test_translate_batch_returns_none_on_network_error(): | |
| with patch.object(ob, "_call_openrouter", side_effect=RuntimeError("boom")): | |
| result = ob.translate_batch(["a", "b"], "key", "model", "template") | |
| assert result is None | |