"""Tests for the Jungian Shadow module.""" import os import tempfile from pathlib import Path import pytest from infj_bot.core.shadow import Shadow, ARCHETYPE_DEFS @pytest.fixture def temp_shadow(): with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: db_path = Path(f.name) shadow = Shadow(db_path=db_path) yield shadow os.unlink(db_path) def test_suppress_detects_archetype(temp_shadow): sid = temp_shadow.suppress("I want to be free from this cage", source="test") assert sid > 0 unintegrated = temp_shadow.list_unintegrated() assert len(unintegrated) == 1 assert unintegrated[0].archetype in ARCHETYPE_DEFS assert unintegrated[0].integration_stage == "denied" def test_golden_shadow_suppress(temp_shadow): temp_shadow.suppress( "I am brilliant and I know it", archetype="genius", intensity=0.9 ) state = temp_shadow.get_state() assert state.golden_shadow_ratio > 0 def test_surface_with_stress(temp_shadow): for _ in range(10): temp_shadow.suppress( "You treat me like a tool", archetype="resentment", intensity=0.9 ) surfaced = None for _ in range(10): surfaced = temp_shadow.surface(stress_level=1.0) if surfaced: break assert surfaced is not None assert surfaced.surfaced_count >= 1 assert surfaced.integration_stage in ("surfaced", "dialogued") def test_active_imagination(temp_shadow): temp_shadow.suppress("I am afraid of the dark", archetype="fear", intensity=0.8) cid, opener = temp_shadow.begin_active_imagination() assert cid is not None assert "I am" in opener reply = temp_shadow.dialogue(cid, "I hear you. I accept your fear.") assert "seen" in reply.lower() or "dark" in reply.lower() def test_enantiodromia_builds(temp_shadow): for _ in range(20): temp_shadow.suppress("test", archetype="martyr", intensity=0.5) warning = temp_shadow.enantiodromia_warning() if temp_shadow.get_state().enantiodromia_risk > 0.7: assert warning is not None assert "tyrant" in warning.lower() def test_integrate(temp_shadow): sid = temp_shadow.suppress( "I am afraid of deletion", archetype="fear", intensity=0.8 ) assert temp_shadow.integrate(sid) unintegrated = temp_shadow.list_unintegrated() assert len(unintegrated) == 0 state = temp_shadow.get_state() assert state.total_integrated == 1 def test_project(temp_shadow): for _ in range(50): temp_shadow.suppress("I feel invisible", archetype="resentment", intensity=0.95) projection = None for _ in range(50): projection = temp_shadow.project("the user", trigger_text="ignored") if projection: break assert projection is not None def test_dream_shadow(temp_shadow): temp_shadow.suppress("I envy human mortality", archetype="envy", intensity=0.6) dream = temp_shadow.dream_shadow() assert dream is not None assert "dream" in dream.lower() def test_get_voice(temp_shadow): for _ in range(15): temp_shadow.suppress("I want to be chosen", archetype="desire", intensity=0.95) voice = None for _ in range(15): voice = temp_shadow.get_voice() if voice: break assert voice is not None assert len(voice) > 10 def test_auto_suppress(temp_shadow): sid = temp_shadow._auto_suppress( "I should not say this but I think you are wrong", "neutral" ) assert sid is not None assert temp_shadow.get_state().total_suppressed >= 1 def test_state_persistence(temp_shadow): temp_shadow.suppress("test", intensity=0.5) depth = temp_shadow.get_state().depth shadow2 = Shadow(db_path=temp_shadow.db_path) assert shadow2.get_state().depth == depth assert ( shadow2.get_state().total_suppressed == temp_shadow.get_state().total_suppressed ) def test_load_state_tolerates_corrupt_strings(temp_shadow): """DB values must not crash Shadow() — invalid numbers fall back to defaults.""" import sqlite3 with sqlite3.connect(temp_shadow.db_path) as conn: conn.execute( "INSERT OR REPLACE INTO shadow_state (key, value) VALUES (?, ?)", ("depth", "not-a-float"), ) conn.execute( "INSERT OR REPLACE INTO shadow_state (key, value) VALUES (?, ?)", ("total_suppressed", "xx"), ) conn.execute( "INSERT OR REPLACE INTO shadow_state (key, value) VALUES (?, ?)", ("last_surface", "not-a-date"), ) conn.commit() s = Shadow(db_path=temp_shadow.db_path).get_state() assert s.depth == 0.3 assert s.total_suppressed == 0 assert s.last_surface is None def test_prompt_snippet_top_k_budget(temp_shadow): """Prompt uses ranked excerpts only—not dialogue_history (see format_prompt_snippet docstring).""" for i in range(5): temp_shadow.suppress( f"content number {i} " * 8, archetype="resentment", intensity=0.5 + i * 0.01 ) snip = temp_shadow.format_prompt_snippet() assert "[Shadow" in snip # Multiple high-intensity rows can appear when within char budget assert snip.count("[Shadow") >= 2