| """Tests for the sentence composer fallback path (no API keys configured).""" |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from signbridge.composer.sentence import _naive_join, _strip_quotes, compose_sentence |
|
|
|
|
| class TestNaiveJoin: |
| def test_empty(self) -> None: |
| assert _naive_join([]) == "" |
|
|
| def test_single_lowercase_gloss(self) -> None: |
| assert _naive_join(["hello"]) == "Hello." |
|
|
| def test_single_letter(self) -> None: |
| assert _naive_join(["A"]) == "A." |
|
|
| def test_fingerspelled_word(self) -> None: |
| assert _naive_join(["L", "U", "C", "A", "S"]) == "Lucas." |
|
|
| def test_fingerspelled_then_gloss(self) -> None: |
| |
| assert _naive_join(["L", "U", "C", "A", "S", "hello"]) == "Lucas hello." |
|
|
| def test_gloss_with_underscore(self) -> None: |
| |
| assert "thank you" in _naive_join(["thank_you"]).lower() |
|
|
| def test_existing_punctuation_not_doubled(self) -> None: |
| |
| out = _naive_join(["hello"]) |
| assert out.count(".") == 1 |
|
|
|
|
| class TestStripQuotes: |
| def test_no_quotes(self) -> None: |
| assert _strip_quotes("hello") == "hello" |
|
|
| def test_double_quotes(self) -> None: |
| assert _strip_quotes('"hello"') == "hello" |
|
|
| def test_single_quotes(self) -> None: |
| assert _strip_quotes("'hello'") == "hello" |
|
|
| def test_internal_quotes_preserved(self) -> None: |
| assert _strip_quotes('"he said \'hi\'"') == "he said 'hi'" |
|
|
|
|
| class TestComposeSentenceFallback: |
| """Without API keys, compose should fall back to _naive_join.""" |
|
|
| def test_empty(self) -> None: |
| assert compose_sentence([]) == "" |
|
|
| def test_falls_back_to_naive(self) -> None: |
| |
| result = compose_sentence(["H", "I"]) |
| assert result == "Hi." |
|
|
| def test_falls_back_for_glosses(self) -> None: |
| result = compose_sentence(["hello", "name", "L", "U", "C", "A", "S"]) |
| assert "Lucas" in result |
| assert "hello" in result.lower() |
| assert "name" in result.lower() |
|
|
|
|
| class TestComposeSentenceWithMockProvider: |
| """When an API key is set we ATTEMPT to call the provider, but on failure |
| we still fall back. We simulate a working provider by monkeypatching the |
| OpenAI client.""" |
|
|
| def test_provider_returns_clean_sentence(self, monkeypatch: pytest.MonkeyPatch) -> None: |
| monkeypatch.setenv("SIGNBRIDGE_PROVIDER", "openai") |
| monkeypatch.setenv("OPENAI_API_KEY", "test-key") |
|
|
| |
| from signbridge.composer import sentence as sentence_mod |
|
|
| class _FakeChoice: |
| def __init__(self, content: str) -> None: |
| self.message = type("M", (), {"content": content})() |
|
|
| class _FakeResp: |
| def __init__(self, content: str) -> None: |
| self.choices = [_FakeChoice(content)] |
|
|
| class _FakeChat: |
| def __init__(self, content: str) -> None: |
| self._content = content |
| self.completions = self |
|
|
| def create(self, **_: object) -> _FakeResp: |
| return _FakeResp(self._content) |
|
|
| class _FakeClient: |
| def __init__(self, content: str) -> None: |
| self.chat = _FakeChat(content) |
|
|
| monkeypatch.setattr( |
| sentence_mod, |
| "_resolve_client", |
| lambda: (_FakeClient("My name is Lucas."), "test-model"), |
| ) |
| out = compose_sentence(["name", "L", "U", "C", "A", "S"]) |
| assert out == "My name is Lucas." |
|
|
| def test_provider_failure_falls_back(self, monkeypatch: pytest.MonkeyPatch) -> None: |
| from signbridge.composer import sentence as sentence_mod |
|
|
| class _FailingClient: |
| class _FailingChat: |
| completions = type( |
| "_C", |
| (), |
| {"create": staticmethod(lambda **_: (_ for _ in ()).throw(RuntimeError("boom")))}, |
| )() |
| chat = _FailingChat() |
|
|
| monkeypatch.setattr( |
| sentence_mod, "_resolve_client", lambda: (_FailingClient(), "test") |
| ) |
| out = compose_sentence(["hello"]) |
| assert out == "Hello." |
|
|