| import importlib |
| import json |
| from pathlib import Path |
| from typing import Any |
|
|
| from fastapi.testclient import TestClient |
|
|
| import app as babble_app |
|
|
|
|
| class FakeTelegramClient: |
| requests: list[dict[str, Any]] = [] |
| status_code = 200 |
| text = "{\"ok\":true}" |
|
|
| def __init__(self, *args: Any, **kwargs: Any) -> None: |
| del args, kwargs |
|
|
| async def __aenter__(self) -> "FakeTelegramClient": |
| return self |
|
|
| async def __aexit__(self, *args: Any) -> None: |
| del args |
|
|
| async def post(self, url: str, json: dict[str, Any]) -> "FakeTelegramClient": |
| self.requests.append({"url": url, "json": json}) |
| return self |
|
|
|
|
| class TimeoutTelegramClient(FakeTelegramClient): |
| async def post(self, url: str, json: dict[str, Any]) -> "FakeTelegramClient": |
| del url, json |
| raise babble_app.httpx.ConnectTimeout("timeout", request=None) |
|
|
|
|
| def load_app(monkeypatch, **env): |
| monkeypatch.setenv("REQUIRE_MODEL", "false") |
| monkeypatch.setenv("MEMORY_ENABLED", "false") |
| for key, value in env.items(): |
| monkeypatch.setenv(key, value) |
|
|
| return importlib.reload(babble_app) |
|
|
|
|
| def configure_webhook(monkeypatch, **env): |
| reloaded_app = load_app( |
| monkeypatch, |
| WEBHOOK_SECRET="expected-secret", |
| TELEGRAM_BOT_TOKEN="fake-token", |
| LLM_PROVIDER="stub", |
| **env, |
| ) |
| FakeTelegramClient.requests = [] |
| monkeypatch.setattr(reloaded_app.httpx, "AsyncClient", FakeTelegramClient) |
| return reloaded_app |
|
|
|
|
| def test_health_endpoint(monkeypatch) -> None: |
| reloaded_app = load_app(monkeypatch, LLM_PROVIDER="stub") |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.get("/health") |
|
|
| assert response.status_code == 200 |
| body = response.json() |
| assert body["ok"] is True |
| assert body["service"] == "babble" |
| assert body["model"] == "Qwen/Qwen3-0.6B" |
| assert body["loaded"] is False |
| assert body["persona"] == "default" |
|
|
|
|
| def test_persona_endpoint_lists_available_personas(monkeypatch) -> None: |
| reloaded_app = load_app(monkeypatch, LLM_PROVIDER="stub") |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.get("/persona") |
|
|
| assert response.status_code == 200 |
| body = response.json() |
| assert body["active_persona"]["persona_id"] == "default" |
| assert body["active_persona"]["name"] == "Babble" |
| assert "playful" in body["available_personas"] |
|
|
|
|
| def test_menu_endpoint_shows_commands(monkeypatch) -> None: |
| reloaded_app = load_app(monkeypatch, LLM_PROVIDER="stub") |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.get("/menu") |
|
|
| assert response.status_code == 200 |
| text = response.json()["text"] |
| assert "/persona [id]" in text |
| assert "/character key=value ..." in text |
| assert "Available personas" in text |
|
|
|
|
| def test_webhook_rejects_wrong_secret(monkeypatch) -> None: |
| reloaded_app = configure_webhook(monkeypatch) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.post( |
| "/telegram/webhook/wrong-secret", |
| json={"message": {"chat": {"id": 123}, "text": "hello"}}, |
| ) |
|
|
| assert response.status_code == 403 |
| assert FakeTelegramClient.requests == [] |
|
|
|
|
| def test_start_sends_telegram_message_payload(monkeypatch) -> None: |
| reloaded_app = configure_webhook(monkeypatch) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.post( |
| "/telegram/webhook/expected-secret", |
| json={"message": {"chat": {"id": 123}, "text": " /start "}}, |
| ) |
|
|
| assert response.status_code == 200 |
| assert response.json() == {"ok": True, "sent": True} |
| request = FakeTelegramClient.requests[-1] |
| assert request["url"] == "https://api.telegram.org/botfake-token/sendMessage" |
| assert request["json"]["chat_id"] == 123 |
| assert request["json"]["text"] == "Hey, I’m here. What kind of mood are we in tonight?" |
| assert "reply_markup" in request["json"] |
| assert request["json"]["reply_markup"]["keyboard"][0] == [ |
| "Menu", |
| "Characters", |
| "Build Character", |
| ] |
|
|
|
|
| def test_telegram_timeout_falls_back_to_webhook_payload(monkeypatch) -> None: |
| reloaded_app = load_app( |
| monkeypatch, |
| WEBHOOK_SECRET="expected-secret", |
| TELEGRAM_BOT_TOKEN="fake-token", |
| LLM_PROVIDER="stub", |
| ) |
| monkeypatch.setattr(reloaded_app.httpx, "AsyncClient", TimeoutTelegramClient) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.post( |
| "/telegram/webhook/expected-secret", |
| json={"message": {"chat": {"id": 123}, "text": "hello"}}, |
| ) |
|
|
| assert response.status_code == 200 |
| assert response.json() == { |
| "method": "sendMessage", |
| "chat_id": 123, |
| "text": "Babble stub response: backend online and ready.", |
| "reply_markup": { |
| "keyboard": [ |
| ["Menu", "Characters", "Build Character"], |
| ["Warm", "Romantic", "Playful", "Deep"], |
| ["Babble", "Charlotte", "Jaylaa"], |
| ["Memory On", "Reset", "Help"], |
| ], |
| "resize_keyboard": True, |
| "is_persistent": True, |
| "input_field_placeholder": "Tap a button or send a message", |
| }, |
| } |
|
|
|
|
| def test_persona_command_switches_active_persona(monkeypatch) -> None: |
| reloaded_app = configure_webhook(monkeypatch) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.post( |
| "/telegram/webhook/expected-secret", |
| json={"message": {"chat": {"id": 123}, "text": "/persona playful"}}, |
| ) |
|
|
| assert response.status_code == 200 |
| assert FakeTelegramClient.requests[-1]["json"]["text"] == ( |
| "Persona switched to playful (Charlotte)." |
| ) |
|
|
| client.post( |
| "/telegram/webhook/expected-secret", |
| json={"message": {"chat": {"id": 123}, "text": "/menu"}}, |
| ) |
| assert "playful" in FakeTelegramClient.requests[-1]["json"]["text"] |
|
|
|
|
| def test_character_command_updates_custom_profile(monkeypatch) -> None: |
| reloaded_app = configure_webhook(monkeypatch) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.post( |
| "/telegram/webhook/expected-secret", |
| json={ |
| "message": { |
| "chat": {"id": 123}, |
| "text": ( |
| '/character name=Nova age=21 gender=woman ' |
| 'style="teasing and warm" voice=soft mood=curious ' |
| 'boundaries="adult romance only" opening="Come closer."' |
| ), |
| } |
| }, |
| ) |
|
|
| assert response.status_code == 200 |
| text = FakeTelegramClient.requests[-1]["json"]["text"] |
| assert "Character updated." in text |
| assert "Nova" in text |
| assert "Age: 21" in text |
|
|
|
|
| def test_missing_message_does_not_crash(monkeypatch) -> None: |
| reloaded_app = configure_webhook(monkeypatch) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.post("/telegram/webhook/expected-secret", json={"ok": True}) |
|
|
| assert response.status_code == 200 |
| assert response.json() == {"ok": True, "ignored": "missing_chat_id"} |
| assert FakeTelegramClient.requests == [] |
|
|
|
|
| def test_memory_store_persists_turns(tmp_path: Path, monkeypatch) -> None: |
| memory_path = tmp_path / "chat_memory.json" |
| reloaded_app = load_app( |
| monkeypatch, |
| MEMORY_ENABLED="true", |
| MEMORY_PATH=str(memory_path), |
| LLM_PROVIDER="stub", |
| ) |
|
|
| record = reloaded_app._default_session_record() |
| record = reloaded_app._record_turn(record, "user", "hello") |
| record = reloaded_app._record_turn(record, "assistant", "hi there") |
| reloaded_app._set_session_record("123", record) |
|
|
| assert memory_path.exists() |
| payload = json.loads(memory_path.read_text(encoding="utf-8")) |
| assert payload["123"]["memory"]["turns"][-2:] == [ |
| {"role": "user", "text": "hello"}, |
| {"role": "assistant", "text": "hi there"}, |
| ] |
|
|
|
|
| def test_debug_config_reports_persona_and_memory(monkeypatch) -> None: |
| reloaded_app = load_app( |
| monkeypatch, |
| DEBUG="true", |
| LLM_PROVIDER="stub", |
| PERSONA_ID="playful", |
| ) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.get("/debug/config") |
|
|
| assert response.status_code == 200 |
| body = response.json() |
| assert body["persona_id"] == "playful" |
| assert body["persona_name"] == "Charlotte" |
| assert body["memory_enabled"] is False |
| assert body["persona_count"] >= 3 |
|
|
|
|
| def test_debug_model_surfaces_local_model_failure(monkeypatch) -> None: |
| reloaded_app = load_app( |
| monkeypatch, |
| DEBUG="true", |
| LOCAL_MODEL_ENABLED="false", |
| ) |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.get("/debug/model") |
|
|
| assert response.status_code == 200 |
| body = response.json() |
| assert body["ok"] is False |
| assert body["error"] == "RuntimeError" |
| assert body["detail"] == "Local model is disabled" |
|
|
|
|
| def test_model_endpoint_respects_provider_knob(monkeypatch) -> None: |
| reloaded_app = load_app(monkeypatch, LLM_PROVIDER="stub") |
| client = TestClient(reloaded_app.create_app()) |
|
|
| response = client.get("/model") |
|
|
| assert response.status_code == 200 |
| body = response.json() |
| assert body["model_id"] == "Qwen/Qwen3-0.6B" |
| assert body["enabled"] is False |
| assert body["loaded"] is False |
| assert body["persona_id"] == "default" |
| assert body["memory_enabled"] is False |
|
|