metropolis-chess / tests /test_agent_integration.py
Forkei's picture
Phase 3a: username login, character ownership, content rating, Alembic
06fed2f
"""Integration test: Soul mood deltas flow through the match service.
We run a couple of player→engine turns with a stub Soul that reports
known mood_deltas, then verify raw mood moves in the expected direction
and smoothed mood trails behind (exponential smoothing with tau=3).
"""
from __future__ import annotations
import asyncio
from dataclasses import replace
import pytest
from app.db import SessionLocal
from app.director.mood import MoodState, load_mood
from app.engine.registry import reset_engines_for_testing
from app.matches import service
from app.models.character import Character, CharacterState
from app.models.match import Player
from app.redis_client import reset_memory_store_for_testing
from app.schemas.agents import MoodDeltas, SoulResponse
@pytest.fixture(autouse=True)
def _reset_engines_and_store():
reset_engines_for_testing()
reset_memory_store_for_testing()
yield
reset_engines_for_testing()
reset_memory_store_for_testing()
@pytest.fixture
def _mock_only(monkeypatch):
import app.engine.registry as reg
def _fake_available() -> list:
reg._build_default_factories()
return ["mock"]
monkeypatch.setattr("app.matches.service.available_engines", _fake_available)
yield
@pytest.fixture
def _stub_agents(monkeypatch):
"""Stub the agents pipeline to emit a known `confidence +0.1` delta
every engine turn so we can verify raw/smoothed mood evolution."""
from app.matches import service as match_service
from app.agents.soul import SoulResponse as _SR
from app.schemas.agents import MoodDeltas as _MD
def _stub_sync(session, match, character, board_after, engine_result, smoothed, raw):
resp = _SR(
speak=None,
emotion="focused",
emotion_intensity=0.3,
mood_deltas=_MD(confidence=0.1),
referenced_memory_ids=[],
)
# Apply delta ourselves (mirroring real _run_agents_sync behavior).
from app.director.mood import apply_deltas, save_mood, smooth_mood
new_raw = apply_deltas(raw, resp.mood_deltas.to_dict())
new_smoothed = smooth_mood(smoothed, new_raw)
save_mood(match.id, new_raw, smoothed=False)
save_mood(match.id, new_smoothed, smoothed=True)
return match_service.AgentTurnOutcome(surfaced=[], soul=resp)
monkeypatch.setattr(match_service, "_run_agents_sync", _stub_sync)
yield
def _character(session) -> Character:
char = Character(
name="Deltas",
short_description="t",
aggression=5,
risk_tolerance=5,
patience=5,
trash_talk=5,
target_elo=1500,
current_elo=1500,
floor_elo=1500,
max_elo=1800,
state=CharacterState.READY,
)
session.add(char)
session.commit()
return char
def _player(session) -> Player:
from app.auth import generate_guest_username
p = Player(username=generate_guest_username(), display_name="T")
session.add(p)
session.commit()
return p
def test_soul_mood_delta_applied_to_raw_and_smoothed(_mock_only, _stub_agents):
async def _run():
with SessionLocal() as s:
char = _character(s)
player = _player(s)
match = service.create_match(
s, character_id=char.id, player_id=player.id, player_color="white"
)
s.commit()
match_id = match.id
# Capture starting raw/smoothed confidence.
raw_before = load_mood(match_id, smoothed=False)
smoothed_before = load_mood(match_id, smoothed=True)
assert raw_before is not None and smoothed_before is not None
assert abs(raw_before.confidence - 0.5) < 1e-9
assert abs(smoothed_before.confidence - 0.5) < 1e-9
# Play three player→engine pairs.
moves = ["e2e4", "d2d4", "g1f3"]
for uci in moves:
with SessionLocal() as s:
await service.apply_player_move(s, match_id=match_id, uci=uci)
s.commit()
raw_after = load_mood(match_id, smoothed=False)
smoothed_after = load_mood(match_id, smoothed=True)
return raw_before, smoothed_before, raw_after, smoothed_after
raw_before, smoothed_before, raw_after, smoothed_after = asyncio.run(_run())
# Each engine turn adds +0.1 to raw confidence. 3 turns → +0.3 (capped at 1.0).
assert raw_after.confidence > raw_before.confidence + 0.25
# Smoothed trails raw: should be between raw_before and raw_after.
assert smoothed_before.confidence < smoothed_after.confidence < raw_after.confidence
def test_soul_speak_and_memory_ids_land_on_move_row(_mock_only, monkeypatch):
"""When the Soul speaks, the chat + surfaced ids are persisted on the engine move."""
from app.matches import service as match_service
from app.agents.soul import SoulResponse as _SR
from app.schemas.agents import MoodDeltas as _MD, SurfacedMemory
async def _run():
surfaced = [
SurfacedMemory(
memory_id="mem-stub",
narrative_text="hello",
triggers=[],
relevance_tags=[],
emotional_valence=0.0,
scope="character_lore",
score=0.5,
retrieval_reason="stub",
)
]
def _stub_sync(session, match, character, board_after, engine_result, smoothed, raw):
resp = _SR(
speak="Your move was interesting.",
emotion="pleased",
emotion_intensity=0.5,
mood_deltas=_MD(),
referenced_memory_ids=["mem-stub"],
)
return match_service.AgentTurnOutcome(surfaced=surfaced, soul=resp)
monkeypatch.setattr(match_service, "_run_agents_sync", _stub_sync)
with SessionLocal() as s:
char = _character(s)
player = _player(s)
match = service.create_match(
s, character_id=char.id, player_id=player.id, player_color="white"
)
s.commit()
match_id = match.id
with SessionLocal() as s:
player_move, engine_move, outcome = await service.apply_player_move(
s, match_id=match_id, uci="e2e4"
)
s.commit()
return engine_move, outcome
engine_move, outcome = asyncio.run(_run())
assert engine_move.agent_chat_after == "Your move was interesting."
assert engine_move.surfaced_memory_ids == ["mem-stub"]
assert outcome is not None
assert outcome.soul.emotion == "pleased"
assert len(outcome.surfaced) == 1
def test_opponent_note_queued_on_match(_mock_only, monkeypatch):
"""`note_about_opponent` ends up in match.extra_state.pending_opponent_notes."""
from app.matches import service as match_service
from app.agents.soul import SoulResponse as _SR
from app.schemas.agents import MoodDeltas as _MD
async def _run():
def _stub_sync(session, match, character, board_after, engine_result, smoothed, raw):
resp = _SR(
speak=None,
emotion="neutral",
emotion_intensity=0.1,
mood_deltas=_MD(),
note_about_opponent="plays fast in open positions",
referenced_memory_ids=[],
)
return match_service.AgentTurnOutcome(surfaced=[], soul=resp)
monkeypatch.setattr(match_service, "_run_agents_sync", _stub_sync)
with SessionLocal() as s:
char = _character(s)
player = _player(s)
match = service.create_match(
s, character_id=char.id, player_id=player.id, player_color="white"
)
s.commit()
match_id = match.id
with SessionLocal() as s:
await service.apply_player_move(s, match_id=match_id, uci="e2e4")
s.commit()
with SessionLocal() as s:
return service.get_match(s, match_id)
match = asyncio.run(_run())
notes = (match.extra_state or {}).get("pending_opponent_notes", [])
assert notes
assert notes[0]["note"] == "plays fast in open positions"