Implements the processual architecture based on Whitehead's Process Philosophy and Peirce's Semiotics. Core paradigm: "L'espace latent pense. Le LLM traduit." (The latent space thinks. The LLM translates.) Phase 1-4: Core semiotic cycle - StateTensor 8x1024 (8 Peircean dimensions) - Dissonance computation with hard negatives - Fixation via 4 Peircean methods (Tenacity, Authority, A Priori, Science) - LatentEngine orchestrating the full cycle Phase 5: StateToLanguage - LLM as pure translator (zero-reasoning, T=0) - Projection on interpretable directions - Reasoning markers detection (Amendment #4) Phase 6: Vigilance - x_ref (David) as guard-rail, NOT attractor - Drift detection per dimension and globally - Alerts: ok, warning, critical Phase 7: Autonomous Daemon - Two modes: CONVERSATION (always verbalize), AUTONOMOUS (~1000 cycles/day) - Amendment #5: 50% probability on unresolved impacts - TriggerGenerator with weighted random selection Phase 8: Integration & Metrics - ProcessMetrics for daily/weekly reports - Health status monitoring - Integration tests validating all modules 297 tests passing, version 0.7.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
843 lines
27 KiB
Python
843 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests pour le module daemon - Phase 7.
|
|
|
|
Le daemon d'individuation autonome :
|
|
1. Mode CONVERSATION : toujours verbalise
|
|
2. Mode AUTONOME : pensee silencieuse (~1000 cycles/jour)
|
|
3. Amendment #5 : Rumination sur impacts non resolus
|
|
|
|
Executer: pytest ikario_processual/tests/test_daemon.py -v
|
|
"""
|
|
|
|
import asyncio
|
|
import numpy as np
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
from ikario_processual.state_tensor import StateTensor, DIMENSION_NAMES, EMBEDDING_DIM
|
|
from ikario_processual.dissonance import DissonanceResult
|
|
from ikario_processual.fixation import FixationResult
|
|
from ikario_processual.latent_engine import CycleResult, LatentEngine
|
|
from ikario_processual.vigilance import VigilanceSystem, VigilanceAlert
|
|
from ikario_processual.state_to_language import StateToLanguage, TranslationResult
|
|
from ikario_processual.daemon import (
|
|
TriggerType,
|
|
DaemonMode,
|
|
DaemonConfig,
|
|
DaemonStats,
|
|
Trigger,
|
|
VerbalizationEvent,
|
|
TriggerGenerator,
|
|
IkarioDaemon,
|
|
create_daemon,
|
|
)
|
|
|
|
|
|
def create_random_tensor(state_id: int = 0, seed: int = None) -> StateTensor:
|
|
"""Cree un tenseur avec des vecteurs aleatoires normalises."""
|
|
if seed is not None:
|
|
np.random.seed(seed)
|
|
|
|
tensor = StateTensor(
|
|
state_id=state_id,
|
|
timestamp=datetime.now().isoformat(),
|
|
)
|
|
for dim_name in DIMENSION_NAMES:
|
|
v = np.random.randn(EMBEDDING_DIM)
|
|
v = v / np.linalg.norm(v)
|
|
setattr(tensor, dim_name, v)
|
|
return tensor
|
|
|
|
|
|
def create_mock_cycle_result(
|
|
state_id: int = 1,
|
|
should_verbalize: bool = False,
|
|
verbalization_reason: str = "silent_processing",
|
|
dissonance_total: float = 0.3,
|
|
) -> CycleResult:
|
|
"""Cree un CycleResult mock."""
|
|
tensor = create_random_tensor(state_id=state_id)
|
|
|
|
dissonance = DissonanceResult(
|
|
total=dissonance_total,
|
|
base_dissonance=dissonance_total * 0.8,
|
|
contradiction_score=0.0,
|
|
novelty_penalty=0.0,
|
|
is_choc=dissonance_total > 0.3,
|
|
dissonances_by_dimension={},
|
|
hard_negatives=[],
|
|
max_similarity_to_corpus=0.5,
|
|
rag_results_count=5,
|
|
)
|
|
|
|
fixation = FixationResult(
|
|
delta=np.zeros(EMBEDDING_DIM),
|
|
magnitude=0.0005,
|
|
was_clamped=False,
|
|
contributions={'tenacity': 0, 'authority': 0, 'apriori': 0, 'science': 0.0005},
|
|
)
|
|
|
|
return CycleResult(
|
|
new_state=tensor,
|
|
previous_state_id=state_id - 1,
|
|
dissonance=dissonance,
|
|
fixation=fixation,
|
|
impacts=[],
|
|
thoughts=[],
|
|
should_verbalize=should_verbalize,
|
|
verbalization_reason=verbalization_reason,
|
|
processing_time_ms=50,
|
|
cycle_number=state_id,
|
|
)
|
|
|
|
|
|
class TestTriggerType:
|
|
"""Tests pour TriggerType enum."""
|
|
|
|
def test_all_types_exist(self):
|
|
"""Tous les types existent."""
|
|
assert TriggerType.USER.value == "user"
|
|
assert TriggerType.VEILLE.value == "veille"
|
|
assert TriggerType.CORPUS.value == "corpus"
|
|
assert TriggerType.RUMINATION.value == "rumination"
|
|
assert TriggerType.RUMINATION_FREE.value == "rumination_free"
|
|
assert TriggerType.TIMER.value == "timer"
|
|
assert TriggerType.EMPTY.value == "empty"
|
|
|
|
|
|
class TestDaemonMode:
|
|
"""Tests pour DaemonMode enum."""
|
|
|
|
def test_all_modes_exist(self):
|
|
"""Tous les modes existent."""
|
|
assert DaemonMode.CONVERSATION.value == "conversation"
|
|
assert DaemonMode.AUTONOMOUS.value == "autonomous"
|
|
assert DaemonMode.PAUSED.value == "paused"
|
|
|
|
|
|
class TestDaemonConfig:
|
|
"""Tests pour DaemonConfig."""
|
|
|
|
def test_default_config(self):
|
|
"""Configuration par defaut."""
|
|
config = DaemonConfig()
|
|
|
|
assert config.cycle_interval_seconds == 90.0
|
|
assert config.prob_unresolved_impact == 0.50
|
|
assert config.prob_corpus == 0.30
|
|
assert config.prob_rumination_free == 0.20
|
|
|
|
def test_probabilities_sum_to_one(self):
|
|
"""Les probabilites somment a 1."""
|
|
config = DaemonConfig()
|
|
total = config.prob_unresolved_impact + config.prob_corpus + config.prob_rumination_free
|
|
assert np.isclose(total, 1.0)
|
|
|
|
def test_validate_default(self):
|
|
"""La config par defaut est valide."""
|
|
config = DaemonConfig()
|
|
assert config.validate() == True
|
|
|
|
def test_validate_invalid_probabilities(self):
|
|
"""Config invalide si probabilites != 1."""
|
|
config = DaemonConfig(
|
|
prob_unresolved_impact=0.5,
|
|
prob_corpus=0.5,
|
|
prob_rumination_free=0.5, # Total = 1.5
|
|
)
|
|
assert config.validate() == False
|
|
|
|
|
|
class TestDaemonStats:
|
|
"""Tests pour DaemonStats."""
|
|
|
|
def test_initial_stats(self):
|
|
"""Stats initiales a zero."""
|
|
stats = DaemonStats()
|
|
|
|
assert stats.total_cycles == 0
|
|
assert stats.conversation_cycles == 0
|
|
assert stats.autonomous_cycles == 0
|
|
assert stats.verbalizations == 0
|
|
|
|
def test_to_dict(self):
|
|
"""to_dict() fonctionne."""
|
|
stats = DaemonStats()
|
|
stats.total_cycles = 10
|
|
stats.verbalizations = 3
|
|
|
|
d = stats.to_dict()
|
|
|
|
assert d['total_cycles'] == 10
|
|
assert d['verbalizations'] == 3
|
|
assert 'uptime_seconds' in d
|
|
|
|
|
|
class TestTrigger:
|
|
"""Tests pour Trigger."""
|
|
|
|
def test_create_trigger(self):
|
|
"""Creer un trigger."""
|
|
trigger = Trigger(
|
|
type=TriggerType.USER,
|
|
content="Hello Ikario",
|
|
source="user",
|
|
priority=2,
|
|
)
|
|
|
|
assert trigger.type == TriggerType.USER
|
|
assert trigger.content == "Hello Ikario"
|
|
assert trigger.priority == 2
|
|
|
|
def test_to_dict(self):
|
|
"""to_dict() convertit correctement."""
|
|
trigger = Trigger(
|
|
type=TriggerType.CORPUS,
|
|
content="Whitehead on process",
|
|
source="library",
|
|
metadata={'author': 'Whitehead'},
|
|
)
|
|
|
|
d = trigger.to_dict()
|
|
|
|
assert d['type'] == 'corpus'
|
|
assert d['content'] == "Whitehead on process"
|
|
assert d['metadata']['author'] == 'Whitehead'
|
|
|
|
|
|
class TestVerbalizationEvent:
|
|
"""Tests pour VerbalizationEvent."""
|
|
|
|
def test_create_event(self):
|
|
"""Creer un evenement."""
|
|
event = VerbalizationEvent(
|
|
text="Je suis curieux.",
|
|
reason="conversation_mode",
|
|
trigger_type="user",
|
|
state_id=5,
|
|
dissonance=0.4,
|
|
)
|
|
|
|
assert event.text == "Je suis curieux."
|
|
assert event.reason == "conversation_mode"
|
|
|
|
def test_to_dict(self):
|
|
"""to_dict() fonctionne."""
|
|
event = VerbalizationEvent(
|
|
text="Test",
|
|
reason="test",
|
|
trigger_type="user",
|
|
state_id=1,
|
|
dissonance=0.5,
|
|
)
|
|
|
|
d = event.to_dict()
|
|
|
|
assert 'text' in d
|
|
assert 'reason' in d
|
|
assert 'timestamp' in d
|
|
|
|
|
|
class TestTriggerGenerator:
|
|
"""Tests pour TriggerGenerator."""
|
|
|
|
def test_create_generator(self):
|
|
"""Creer un generateur."""
|
|
config = DaemonConfig()
|
|
generator = TriggerGenerator(config)
|
|
|
|
assert generator.config is config
|
|
assert generator.weaviate is None
|
|
|
|
def test_create_user_trigger(self):
|
|
"""Creer un trigger utilisateur."""
|
|
config = DaemonConfig()
|
|
generator = TriggerGenerator(config)
|
|
|
|
trigger = generator.create_user_trigger("Bonjour")
|
|
|
|
assert trigger.type == TriggerType.USER
|
|
assert trigger.content == "Bonjour"
|
|
assert trigger.priority == 2 # Priorite max
|
|
|
|
def test_create_veille_trigger(self):
|
|
"""Creer un trigger de veille."""
|
|
config = DaemonConfig()
|
|
generator = TriggerGenerator(config)
|
|
|
|
trigger = generator.create_veille_trigger(
|
|
title="Decouverte philosophique",
|
|
snippet="Nouvelle interpretation de Whitehead",
|
|
url="https://example.com/news",
|
|
)
|
|
|
|
assert trigger.type == TriggerType.VEILLE
|
|
assert "Decouverte philosophique" in trigger.content
|
|
assert trigger.metadata['url'] == "https://example.com/news"
|
|
|
|
def test_fallback_trigger_without_weaviate(self):
|
|
"""Sans Weaviate, retourne trigger fallback."""
|
|
config = DaemonConfig()
|
|
generator = TriggerGenerator(config)
|
|
|
|
async def run_test():
|
|
trigger = await generator.generate_autonomous_trigger()
|
|
# Sans Weaviate, tous les generateurs font fallback
|
|
assert trigger.type in (TriggerType.CORPUS, TriggerType.RUMINATION_FREE, TriggerType.EMPTY)
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestTriggerGeneratorAmendment5:
|
|
"""Tests pour Amendment #5 : Rumination sur impacts non resolus."""
|
|
|
|
def test_probabilities_prioritize_impacts(self):
|
|
"""Les probabilites priorisent les impacts (50%)."""
|
|
config = DaemonConfig()
|
|
|
|
assert config.prob_unresolved_impact > config.prob_corpus
|
|
assert config.prob_unresolved_impact > config.prob_rumination_free
|
|
assert config.prob_unresolved_impact == 0.50
|
|
|
|
def test_old_impact_has_high_priority(self):
|
|
"""Impact ancien (>7j) a priorite haute."""
|
|
config = DaemonConfig()
|
|
generator = TriggerGenerator(config)
|
|
|
|
# Simuler un impact ancien via metadata
|
|
trigger = Trigger(
|
|
type=TriggerType.RUMINATION,
|
|
content="Tension non resolue",
|
|
metadata={
|
|
'days_unresolved': 10,
|
|
'is_old_tension': True,
|
|
},
|
|
priority=1,
|
|
)
|
|
|
|
assert trigger.priority == 1
|
|
assert trigger.metadata['is_old_tension'] is True
|
|
|
|
|
|
class TestIkarioDaemon:
|
|
"""Tests pour IkarioDaemon."""
|
|
|
|
def create_mock_daemon(self) -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
# Mock LatentEngine
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result())
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
# Mock VigilanceSystem
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
# Mock StateToLanguage
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Je suis curieux.",
|
|
projections={},
|
|
output_type="response",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=DaemonConfig(cycle_interval_seconds=0.1), # Rapide pour tests
|
|
)
|
|
|
|
def test_create_daemon(self):
|
|
"""Creer un daemon."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
assert daemon.running is False
|
|
assert daemon.mode == DaemonMode.PAUSED
|
|
assert daemon.stats.total_cycles == 0
|
|
|
|
def test_initial_stats(self):
|
|
"""Stats initiales."""
|
|
daemon = self.create_mock_daemon()
|
|
stats = daemon.get_stats()
|
|
|
|
assert stats['total_cycles'] == 0
|
|
assert stats['conversation_cycles'] == 0
|
|
assert stats['autonomous_cycles'] == 0
|
|
|
|
def test_is_running_property(self):
|
|
"""Propriete is_running."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
assert daemon.is_running is False
|
|
|
|
def test_current_mode_property(self):
|
|
"""Propriete current_mode."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
assert daemon.current_mode == DaemonMode.PAUSED
|
|
|
|
|
|
class TestDaemonStartStop:
|
|
"""Tests pour start/stop du daemon."""
|
|
|
|
def create_mock_daemon(self) -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result())
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Test",
|
|
projections={},
|
|
output_type="response",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=DaemonConfig(
|
|
cycle_interval_seconds=0.05,
|
|
vigilance_interval_seconds=0.1,
|
|
),
|
|
)
|
|
|
|
def test_start_stop(self):
|
|
"""Demarrer et arreter le daemon."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
await daemon.start()
|
|
assert daemon.running is True
|
|
assert daemon.mode == DaemonMode.AUTONOMOUS
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
await daemon.stop()
|
|
assert daemon.running is False
|
|
assert daemon.mode == DaemonMode.PAUSED
|
|
|
|
asyncio.run(run_test())
|
|
|
|
def test_run_with_duration(self):
|
|
"""Executer le daemon avec duree limitee."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
await daemon.run(duration_seconds=0.2)
|
|
assert daemon.running is False
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestConversationMode:
|
|
"""Tests pour le mode conversation."""
|
|
|
|
def create_mock_daemon(self) -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result(
|
|
should_verbalize=True,
|
|
verbalization_reason="conversation_mode",
|
|
))
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Je suis curieux de cette question.",
|
|
projections={'epistemic': {'curiosity': 0.7}},
|
|
output_type="response",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
)
|
|
|
|
def test_conversation_always_verbalizes(self):
|
|
"""Mode conversation verbalise toujours."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
event = await daemon.send_message("Qu'est-ce que Whitehead?")
|
|
|
|
assert event.text == "Je suis curieux de cette question."
|
|
assert event.reason == "conversation_mode"
|
|
assert daemon.stats.conversation_cycles == 1
|
|
assert daemon.stats.verbalizations == 1
|
|
|
|
asyncio.run(run_test())
|
|
|
|
def test_translator_called_with_context(self):
|
|
"""Le traducteur recoit le contexte."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
await daemon.send_message("Test message")
|
|
|
|
# Verifier que translate a ete appele
|
|
daemon.translator.translate.assert_called()
|
|
|
|
# Verifier les arguments
|
|
call_kwargs = daemon.translator.translate.call_args.kwargs
|
|
assert call_kwargs['output_type'] == 'response'
|
|
assert 'Test message' in call_kwargs['context']
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestAutonomousMode:
|
|
"""Tests pour le mode autonome."""
|
|
|
|
def create_mock_daemon(self, should_verbalize: bool = False) -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result(
|
|
should_verbalize=should_verbalize,
|
|
verbalization_reason="high_dissonance_discovery" if should_verbalize else "silent_processing",
|
|
dissonance_total=0.7 if should_verbalize else 0.2,
|
|
))
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Decouverte interessante.",
|
|
projections={},
|
|
output_type="autonomous_verbalization",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=DaemonConfig(
|
|
cycle_interval_seconds=0.05, # Tres rapide pour tests
|
|
vigilance_interval_seconds=1.0,
|
|
),
|
|
)
|
|
|
|
def test_autonomous_silent_processing(self):
|
|
"""Mode autonome traite silencieusement par defaut."""
|
|
daemon = self.create_mock_daemon(should_verbalize=False)
|
|
|
|
async def run_test():
|
|
await daemon.start()
|
|
await asyncio.sleep(0.2) # Quelques cycles
|
|
await daemon.stop()
|
|
|
|
# Doit avoir fait des cycles autonomes
|
|
assert daemon.stats.autonomous_cycles > 0
|
|
# Mais pas de verbalisation
|
|
assert daemon.stats.verbalizations == 0
|
|
assert daemon.stats.silent_cycles > 0
|
|
|
|
asyncio.run(run_test())
|
|
|
|
def test_autonomous_verbalizes_on_discovery(self):
|
|
"""Mode autonome verbalise sur decouverte importante."""
|
|
daemon = self.create_mock_daemon(should_verbalize=True)
|
|
|
|
async def run_test():
|
|
await daemon.start()
|
|
await asyncio.sleep(0.2) # Quelques cycles
|
|
await daemon.stop()
|
|
|
|
# Doit avoir verbalise
|
|
assert daemon.stats.verbalizations > 0
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestVigilanceLoop:
|
|
"""Tests pour la boucle de vigilance."""
|
|
|
|
def create_mock_daemon(self, alert_level: str = "ok") -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result())
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(
|
|
level=alert_level,
|
|
message=f"Test alert {alert_level}",
|
|
))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Test",
|
|
projections={},
|
|
output_type="response",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=DaemonConfig(
|
|
cycle_interval_seconds=1.0,
|
|
vigilance_interval_seconds=0.05, # Rapide pour tests
|
|
),
|
|
)
|
|
|
|
def test_vigilance_checks_drift(self):
|
|
"""La boucle vigilance verifie la derive."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
await daemon.start()
|
|
await asyncio.sleep(0.2)
|
|
await daemon.stop()
|
|
|
|
# check_drift doit avoir ete appele
|
|
daemon.vigilance.check_drift.assert_called()
|
|
|
|
asyncio.run(run_test())
|
|
|
|
def test_vigilance_counts_alerts(self):
|
|
"""Les alertes sont comptees."""
|
|
daemon = self.create_mock_daemon(alert_level="warning")
|
|
|
|
async def run_test():
|
|
await daemon.start()
|
|
await asyncio.sleep(0.2)
|
|
await daemon.stop()
|
|
|
|
assert daemon.stats.vigilance_alerts > 0
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestNotificationCallback:
|
|
"""Tests pour le callback de notification."""
|
|
|
|
def test_callback_called_on_autonomous_verbalization(self):
|
|
"""Le callback est appele sur verbalisation autonome."""
|
|
# Mock callback
|
|
callback = AsyncMock()
|
|
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result(
|
|
should_verbalize=True,
|
|
verbalization_reason="high_dissonance",
|
|
))
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Notification test",
|
|
projections={},
|
|
output_type="autonomous",
|
|
))
|
|
|
|
daemon = IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=DaemonConfig(
|
|
cycle_interval_seconds=0.05,
|
|
vigilance_interval_seconds=1.0,
|
|
),
|
|
notification_callback=callback,
|
|
)
|
|
|
|
async def run_test():
|
|
await daemon.start()
|
|
await asyncio.sleep(0.2)
|
|
await daemon.stop()
|
|
|
|
# Le callback doit avoir ete appele
|
|
callback.assert_called()
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestVerbalizationHistory:
|
|
"""Tests pour l'historique des verbalisations."""
|
|
|
|
def create_mock_daemon(self) -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result())
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Test response",
|
|
projections={},
|
|
output_type="response",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
)
|
|
|
|
def test_history_records_conversations(self):
|
|
"""L'historique enregistre les conversations."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
await daemon.send_message("Message 1")
|
|
await daemon.send_message("Message 2")
|
|
|
|
history = daemon.get_verbalization_history()
|
|
|
|
assert len(history) == 2
|
|
assert all('text' in h for h in history)
|
|
|
|
asyncio.run(run_test())
|
|
|
|
def test_history_limit(self):
|
|
"""L'historique respecte la limite."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
for i in range(15):
|
|
await daemon.send_message(f"Message {i}")
|
|
|
|
history = daemon.get_verbalization_history(limit=5)
|
|
|
|
assert len(history) == 5
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
class TestCreateDaemonFactory:
|
|
"""Tests pour la factory create_daemon."""
|
|
|
|
def test_create_daemon_factory(self):
|
|
"""create_daemon cree un daemon."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
|
|
daemon = create_daemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
)
|
|
|
|
assert isinstance(daemon, IkarioDaemon)
|
|
assert daemon.engine is mock_engine
|
|
assert daemon.vigilance is mock_vigilance
|
|
assert daemon.translator is mock_translator
|
|
|
|
def test_create_daemon_with_config(self):
|
|
"""create_daemon accepte une config."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
|
|
config = DaemonConfig(cycle_interval_seconds=60.0)
|
|
|
|
daemon = create_daemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=config,
|
|
)
|
|
|
|
assert daemon.config.cycle_interval_seconds == 60.0
|
|
|
|
|
|
class TestCycleRate:
|
|
"""Tests pour le taux de cycles (~1000/jour)."""
|
|
|
|
def test_default_cycle_rate(self):
|
|
"""Le taux par defaut est ~1000 cycles/jour."""
|
|
config = DaemonConfig()
|
|
|
|
# 86400 secondes/jour / 90 secondes/cycle = 960 cycles/jour
|
|
cycles_per_day = 86400 / config.cycle_interval_seconds
|
|
|
|
assert 900 < cycles_per_day < 1100 # ~1000 cycles/jour
|
|
|
|
|
|
class TestStatsTracking:
|
|
"""Tests pour le suivi des statistiques."""
|
|
|
|
def create_mock_daemon(self) -> IkarioDaemon:
|
|
"""Cree un daemon avec mocks."""
|
|
mock_engine = MagicMock(spec=LatentEngine)
|
|
mock_engine.run_cycle = AsyncMock(return_value=create_mock_cycle_result())
|
|
mock_engine._get_current_state = MagicMock(return_value=create_random_tensor())
|
|
|
|
mock_vigilance = MagicMock(spec=VigilanceSystem)
|
|
mock_vigilance.check_drift = MagicMock(return_value=VigilanceAlert(level="ok"))
|
|
|
|
mock_translator = MagicMock(spec=StateToLanguage)
|
|
mock_translator.translate = AsyncMock(return_value=TranslationResult(
|
|
text="Test",
|
|
projections={},
|
|
output_type="response",
|
|
))
|
|
|
|
return IkarioDaemon(
|
|
latent_engine=mock_engine,
|
|
vigilance=mock_vigilance,
|
|
translator=mock_translator,
|
|
config=DaemonConfig(
|
|
cycle_interval_seconds=0.05,
|
|
vigilance_interval_seconds=1.0,
|
|
),
|
|
)
|
|
|
|
def test_total_cycles_tracked(self):
|
|
"""Les cycles totaux sont suivis."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
# Envoyer quelques messages
|
|
await daemon.send_message("Test 1")
|
|
await daemon.send_message("Test 2")
|
|
|
|
stats = daemon.get_stats()
|
|
|
|
# Au moins 2 cycles (les conversations)
|
|
assert stats['total_cycles'] >= 2
|
|
|
|
asyncio.run(run_test())
|
|
|
|
def test_last_cycle_time_updated(self):
|
|
"""last_cycle_time est mis a jour."""
|
|
daemon = self.create_mock_daemon()
|
|
|
|
async def run_test():
|
|
await daemon.send_message("Test")
|
|
|
|
stats = daemon.get_stats()
|
|
|
|
assert stats['last_cycle_time'] != ""
|
|
|
|
asyncio.run(run_test())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|