Add Ikario Architecture v2 - Phases 1-8 complete

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>
This commit is contained in:
2026-02-01 21:18:40 +01:00
parent 9c2145bcf2
commit f6fe71e2f7
19 changed files with 9887 additions and 9 deletions

View File

@@ -0,0 +1,842 @@
#!/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"])