Spaces:
Running
Running
| """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 | |
| def _reset_engines_and_store(): | |
| reset_engines_for_testing() | |
| reset_memory_store_for_testing() | |
| yield | |
| reset_engines_for_testing() | |
| reset_memory_store_for_testing() | |
| 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 | |
| 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" | |