mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-07-09 20:08:42 +02:00
- **Per-Scene Agent Overrides** — Override agent configuration for a single scene without touching your global config. - **Message Revision History** — Browse and continue from previous regenerations / revisions. - **Scene Perspective Overrides** — Set distinct narrative perspectives for the player, NPCs, and narrator. - **Durable World State Snapshot** — The world state snapshot now persists and updates entities across refreshes. - **World State Highlights** — Characters, objects and places marked by the current world state snapshot are now clickable in the recent message(s) for additional detail. Plus 30 improvements and 17 bug fixes
149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
"""Unit tests for talemate.client.google.GoogleClient.
|
|
|
|
Focused on the setup-incomplete status handling for issue #40:
|
|
|
|
- The "Setup incomplete" sentinel must not leak into the emitted model name.
|
|
The frontend persists the emitted model name back into the client `model`
|
|
field, so leaking the sentinel corrupts the config and causes every Google
|
|
request to fail with HTTP 400.
|
|
- A config that already has the leaked sentinel persisted in its `model` field
|
|
must be healed to "no model selected" rather than sent to the API verbatim.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import talemate.config.state as config_state
|
|
from talemate.client import google as google_module
|
|
from talemate.client.google import (
|
|
ClientConfig,
|
|
GoogleClient,
|
|
SETUP_INCOMPLETE_MODEL,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def cfg_isolation():
|
|
"""Snapshot/restore the config sections these tests mutate."""
|
|
saved_clients = dict(config_state.CONFIG.clients)
|
|
saved_api_key = config_state.CONFIG.google.api_key
|
|
saved_creds = config_state.CONFIG.google.gcloud_credentials_path
|
|
saved_location = config_state.CONFIG.google.gcloud_location
|
|
yield
|
|
config_state.CONFIG.clients.clear()
|
|
config_state.CONFIG.clients.update(saved_clients)
|
|
config_state.CONFIG.google.api_key = saved_api_key
|
|
config_state.CONFIG.google.gcloud_credentials_path = saved_creds
|
|
config_state.CONFIG.google.gcloud_location = saved_location
|
|
|
|
|
|
@pytest.fixture
|
|
def no_google_credentials(cfg_isolation):
|
|
"""Ensure the Google service is treated as not configured."""
|
|
config_state.CONFIG.google.api_key = None
|
|
config_state.CONFIG.google.gcloud_credentials_path = None
|
|
config_state.CONFIG.google.gcloud_location = None
|
|
|
|
|
|
@pytest.fixture
|
|
def capture_emit(monkeypatch):
|
|
"""Capture every emit() call made by the google client module."""
|
|
calls = []
|
|
|
|
def _fake_emit(*args, **kwargs):
|
|
calls.append({"args": args, "kwargs": kwargs})
|
|
|
|
monkeypatch.setattr(google_module, "emit", _fake_emit)
|
|
return calls
|
|
|
|
|
|
def _register(name: str, **kwargs) -> ClientConfig:
|
|
cfg = ClientConfig(type="google", name=name, **kwargs)
|
|
config_state.CONFIG.clients[name] = cfg
|
|
return cfg
|
|
|
|
|
|
class TestModelSentinelHealing:
|
|
def test_persisted_sentinel_model_resolves_to_none(self, no_google_credentials):
|
|
_register("g_healed", model=SETUP_INCOMPLETE_MODEL)
|
|
client = GoogleClient(name="g_healed")
|
|
# The leaked sentinel must not be treated as a real model.
|
|
assert client.model is None
|
|
assert client.model_name is None
|
|
|
|
def test_real_model_passes_through(self, no_google_credentials):
|
|
_register("g_real", model="gemini-3.5-flash")
|
|
client = GoogleClient(name="g_real")
|
|
assert client.model == "gemini-3.5-flash"
|
|
assert client.model_name == "gemini-3.5-flash"
|
|
|
|
|
|
class TestEmitStatus:
|
|
def test_not_ready_keeps_real_model_name(self, no_google_credentials, capture_emit):
|
|
_register("g_not_ready", model="gemini-3.5-flash")
|
|
client = GoogleClient(name="g_not_ready")
|
|
|
|
client.emit_status()
|
|
|
|
assert client.current_status == "error"
|
|
emitted = capture_emit[-1]["kwargs"]
|
|
# The model name shown to (and persisted by) the frontend must stay the
|
|
# real model, never the "Setup incomplete" sentinel.
|
|
assert emitted["details"] == "gemini-3.5-flash"
|
|
assert emitted["details"] != SETUP_INCOMPLETE_MODEL
|
|
# The incomplete-setup state is communicated via error_message instead.
|
|
assert emitted["data"]["error_message"] == "Setup incomplete"
|
|
assert emitted["data"]["error_action"] is not None
|
|
|
|
def test_ready_with_model_is_idle(self, cfg_isolation, capture_emit):
|
|
config_state.CONFIG.google.api_key = "test-key"
|
|
config_state.CONFIG.google.gcloud_credentials_path = None
|
|
config_state.CONFIG.google.gcloud_location = None
|
|
_register("g_ready", model="gemini-3.5-flash")
|
|
client = GoogleClient(name="g_ready")
|
|
|
|
client.emit_status()
|
|
|
|
assert client.current_status == "idle"
|
|
emitted = capture_emit[-1]["kwargs"]
|
|
assert emitted["details"] == "gemini-3.5-flash"
|
|
assert emitted["data"]["error_message"] is None
|
|
assert emitted["data"]["error_action"] is None
|
|
|
|
def test_healed_sentinel_reports_no_model_loaded(self, cfg_isolation, capture_emit):
|
|
# Key is set (so `ready` is True) but the persisted model is the leaked
|
|
# sentinel. Instead of shipping it to Google, the client must surface a
|
|
# clean "No model loaded" state.
|
|
config_state.CONFIG.google.api_key = "test-key"
|
|
config_state.CONFIG.google.gcloud_credentials_path = None
|
|
config_state.CONFIG.google.gcloud_location = None
|
|
_register("g_healed_ready", model=SETUP_INCOMPLETE_MODEL)
|
|
client = GoogleClient(name="g_healed_ready")
|
|
|
|
client.emit_status()
|
|
|
|
assert client.current_status == "error"
|
|
emitted = capture_emit[-1]["kwargs"]
|
|
assert emitted["data"]["error_message"] == "No model loaded"
|
|
assert emitted["details"] != SETUP_INCOMPLETE_MODEL
|
|
|
|
|
|
class TestGenerateGuards:
|
|
@pytest.mark.asyncio
|
|
async def test_generate_raises_when_not_ready(self, no_google_credentials):
|
|
_register("g_gen_not_ready", model="gemini-3.5-flash")
|
|
client = GoogleClient(name="g_gen_not_ready")
|
|
with pytest.raises(Exception, match="setup incomplete"):
|
|
await client.generate("prompt", {}, "conversation")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_raises_when_no_model(self, cfg_isolation):
|
|
config_state.CONFIG.google.api_key = "test-key"
|
|
config_state.CONFIG.google.gcloud_credentials_path = None
|
|
config_state.CONFIG.google.gcloud_location = None
|
|
_register("g_gen_no_model", model=SETUP_INCOMPLETE_MODEL)
|
|
client = GoogleClient(name="g_gen_no_model")
|
|
with pytest.raises(Exception, match="no model selected"):
|
|
await client.generate("prompt", {}, "conversation")
|