Files
linear-coding-agent/ikario_processual/tests/test_vigilance.py
David Blanc Brioir f6fe71e2f7 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>
2026-02-01 22:30:19 +01:00

484 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Tests pour le module de vigilance - Phase 6.
Systeme de vigilance x_ref (David) :
1. x_ref N'EST PAS un attracteur (Ikario ne tend pas vers David)
2. x_ref EST un garde-fou (alerte si distance > seuil)
3. Alertes : ok, warning, critical
Executer: pytest ikario_processual/tests/test_vigilance.py -v
"""
import json
import numpy as np
import pytest
import tempfile
from datetime import datetime
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.vigilance import (
VigilanceAlert,
VigilanceConfig,
VigilanceSystem,
DavidReference,
VigilanceVisualizer,
create_vigilance_system,
)
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_similar_tensor(reference: StateTensor, noise: float = 0.01) -> StateTensor:
"""Cree un tenseur similaire a la reference avec un peu de bruit."""
tensor = reference.copy()
tensor.state_id = reference.state_id + 1
for dim_name in DIMENSION_NAMES:
vec = getattr(tensor, dim_name).copy()
# Ajouter du bruit
vec += np.random.randn(EMBEDDING_DIM) * noise
# Re-normaliser
vec = vec / np.linalg.norm(vec)
setattr(tensor, dim_name, vec)
return tensor
def create_different_tensor(reference: StateTensor, offset: float = 0.5) -> StateTensor:
"""Cree un tenseur different de la reference."""
tensor = reference.copy()
tensor.state_id = reference.state_id + 1
for dim_name in DIMENSION_NAMES:
# Vecteur orthogonal approximatif
vec = np.random.randn(EMBEDDING_DIM)
vec = vec / np.linalg.norm(vec)
setattr(tensor, dim_name, vec)
return tensor
class TestVigilanceAlert:
"""Tests pour VigilanceAlert."""
def test_create_alert(self):
"""Creer une alerte."""
alert = VigilanceAlert(
level="warning",
message="Derive detectee",
cumulative_drift=0.015,
state_id=5,
)
assert alert.level == "warning"
assert alert.cumulative_drift == 0.015
assert alert.is_alert is True
def test_ok_not_alert(self):
"""'ok' n'est pas une alerte."""
alert = VigilanceAlert(level="ok")
assert alert.is_alert is False
def test_warning_is_alert(self):
"""'warning' est une alerte."""
alert = VigilanceAlert(level="warning")
assert alert.is_alert is True
def test_critical_is_alert(self):
"""'critical' est une alerte."""
alert = VigilanceAlert(level="critical")
assert alert.is_alert is True
def test_to_dict(self):
"""to_dict() fonctionne."""
alert = VigilanceAlert(
level="critical",
message="Test",
dimensions={'firstness': 0.1},
cumulative_drift=0.025,
)
d = alert.to_dict()
assert 'level' in d
assert 'message' in d
assert 'dimensions' in d
assert d['cumulative_drift'] == 0.025
class TestVigilanceConfig:
"""Tests pour VigilanceConfig."""
def test_default_config(self):
"""Configuration par defaut."""
config = VigilanceConfig()
assert config.threshold_cumulative == 0.01 # 1%
assert config.threshold_per_cycle == 0.002 # 0.2%
assert config.threshold_per_dimension == 0.05 # 5%
assert config.critical_multiplier == 2.0
def test_validate_default(self):
"""La config par defaut est valide."""
config = VigilanceConfig()
assert config.validate() is True
def test_validate_invalid(self):
"""Config invalide."""
config = VigilanceConfig(threshold_cumulative=2.0) # > 1
assert config.validate() is False
class TestVigilanceSystem:
"""Tests pour VigilanceSystem."""
def test_create_system(self):
"""Creer un systeme de vigilance."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
assert system.x_ref is x_ref
assert system.cumulative_drift == 0.0
assert len(system.history) == 0
def test_no_drift_when_identical(self):
"""Pas de derive si X_t == x_ref."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
# Premier check avec x_ref lui-meme
alert = system.check_drift(x_ref)
assert alert.level == "ok"
assert alert.cumulative_drift == 0.0
def test_warning_when_drifting(self):
"""Alerte warning quand derive > seuil."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(
x_ref=x_ref,
config=VigilanceConfig(threshold_cumulative=0.001) # Seuil bas
)
# Premier check etablit X_prev
system.check_drift(x_ref)
# Creer un etat different
X_t = create_different_tensor(x_ref)
alert = system.check_drift(X_t)
# Devrait etre au moins warning ou critical
assert alert.level in ("warning", "critical")
def test_critical_when_high_drift(self):
"""Alerte critical quand derive >> seuil."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(
x_ref=x_ref,
config=VigilanceConfig(
threshold_cumulative=0.0001, # Seuil tres bas
critical_multiplier=1.5
)
)
# Premier check
system.check_drift(x_ref)
# Plusieurs checks avec etats differents pour accumuler drift
for i in range(3):
X_t = create_different_tensor(x_ref)
X_t.state_id = i + 1
alert = system.check_drift(X_t)
assert alert.level == "critical"
def test_cumulative_drift_increases(self):
"""La derive cumulative augmente."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
# Premier check
system.check_drift(x_ref)
# Plusieurs checks avec de petites differences
for i in range(5):
X_t = create_similar_tensor(x_ref, noise=0.1)
X_t.state_id = i + 1
system.check_drift(X_t)
assert system.cumulative_drift > 0
def test_reset_cumulative(self):
"""Reset de la derive cumulative."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
# Accumuler de la derive
system.check_drift(x_ref)
X_t = create_different_tensor(x_ref)
system.check_drift(X_t)
assert system.cumulative_drift > 0
# Reset
system.reset_cumulative()
assert system.cumulative_drift == 0.0
def test_history_recorded(self):
"""L'historique des alertes est enregistre."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
for i in range(3):
X_t = create_similar_tensor(x_ref, noise=0.05)
X_t.state_id = i
system.check_drift(X_t)
assert len(system.history) == 3
class TestDistanceCalculations:
"""Tests pour les calculs de distance."""
def test_distance_per_dimension(self):
"""Distance par dimension."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
# Distance avec soi-meme = 0
distances = system._distance_per_dimension(x_ref)
for dim_name, dist in distances.items():
assert np.isclose(dist, 0.0, atol=1e-6)
def test_distance_opposite_vectors(self):
"""Distance avec vecteurs opposes."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
# Creer un tenseur avec vecteurs opposes
X_opposite = x_ref.copy()
for dim_name in DIMENSION_NAMES:
setattr(X_opposite, dim_name, -getattr(x_ref, dim_name))
distances = system._distance_per_dimension(X_opposite)
# Distance cosine avec vecteur oppose = 2 (1 - (-1))
for dim_name, dist in distances.items():
assert np.isclose(dist, 2.0, atol=1e-6)
def test_global_distance_self(self):
"""Distance globale avec soi-meme = 0."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
dist = system._global_distance(x_ref)
assert np.isclose(dist, 0.0, atol=1e-6)
def test_global_distance_different(self):
"""Distance globale avec tenseur different > 0."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
X_different = create_random_tensor(state_id=1, seed=123)
dist = system._global_distance(X_different)
assert dist > 0
class TestTopDriftingDimensions:
"""Tests pour l'identification des dimensions en derive."""
def test_identifies_drifting_dims(self):
"""Identifie les dimensions qui derivent."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
# Creer un tenseur ou certaines dimensions sont tres differentes
X_t = x_ref.copy()
# Inverser seulement 'firstness' et 'valeurs'
X_t.firstness = -x_ref.firstness
X_t.valeurs = -x_ref.valeurs
alert = system.check_drift(X_t)
# Les dimensions inversees devraient etre dans le top
assert 'firstness' in alert.top_drifting_dimensions
assert 'valeurs' in alert.top_drifting_dimensions
class TestDavidReference:
"""Tests pour DavidReference."""
def test_create_from_declared_profile_no_model(self):
"""Creer x_ref depuis profil sans modele d'embedding."""
# Creer un fichier profil temporaire
profile = {
"profile": {
"epistemic": {"curiosity": 8, "certainty": 3},
"affective": {"enthusiasm": 5},
}
}
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(profile, f)
profile_path = f.name
x_ref = DavidReference.create_from_declared_profile(profile_path)
assert x_ref.state_id == -1
assert x_ref.firstness.shape == (EMBEDDING_DIM,)
# Vecteurs normalises
assert np.isclose(np.linalg.norm(x_ref.firstness), 1.0)
def test_create_hybrid_fallback(self):
"""create_hybrid sans weaviate retourne profil declare."""
profile = {"profile": {"epistemic": {"curiosity": 5}}}
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(profile, f)
profile_path = f.name
# Sans weaviate, utilise create_hybrid avec mock
x_declared = DavidReference.create_from_declared_profile(profile_path)
assert x_declared is not None
assert x_declared.state_id == -1
class TestVigilanceVisualizer:
"""Tests pour VigilanceVisualizer."""
def test_format_distance_report(self):
"""format_distance_report genere un rapport."""
x_ref = create_random_tensor(state_id=-1, seed=42)
X_t = create_similar_tensor(x_ref, noise=0.1)
report = VigilanceVisualizer.format_distance_report(X_t, x_ref, 0.005)
assert "RAPPORT VIGILANCE" in report
assert "Derive cumulative" in report
for dim_name in DIMENSION_NAMES:
assert dim_name in report
def test_format_report_includes_bars(self):
"""Le rapport inclut des barres de progression."""
x_ref = create_random_tensor(state_id=-1, seed=42)
X_t = create_different_tensor(x_ref)
report = VigilanceVisualizer.format_distance_report(X_t, x_ref)
# Devrait avoir des barres (caracteres # et -)
assert "#" in report or "-" in report
class TestCreateVigilanceSystem:
"""Tests pour la factory create_vigilance_system."""
def test_create_without_args(self):
"""Creer un systeme sans arguments (mode test)."""
system = create_vigilance_system()
assert system is not None
assert system.x_ref is not None
assert system.x_ref.state_id == -1
def test_create_with_profile(self):
"""Creer un systeme avec profil."""
profile = {"profile": {"epistemic": {"curiosity": 7}}}
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(profile, f)
profile_path = f.name
system = create_vigilance_system(profile_path=profile_path)
assert system is not None
assert system.x_ref.state_id == -1
def test_create_with_custom_config(self):
"""Creer un systeme avec config personnalisee."""
config = VigilanceConfig(
threshold_cumulative=0.02,
threshold_per_cycle=0.005
)
system = create_vigilance_system(config=config)
assert system.config.threshold_cumulative == 0.02
assert system.config.threshold_per_cycle == 0.005
class TestGetStats:
"""Tests pour get_stats."""
def test_initial_stats(self):
"""Stats initiales."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
stats = system.get_stats()
assert stats['cumulative_drift'] == 0.0
assert stats['total_checks'] == 0
assert stats['alerts_count'] == {'ok': 0, 'warning': 0, 'critical': 0}
def test_stats_after_checks(self):
"""Stats apres plusieurs checks."""
x_ref = create_random_tensor(state_id=-1, seed=42)
system = VigilanceSystem(x_ref=x_ref)
for i in range(5):
X_t = create_similar_tensor(x_ref, noise=0.05)
X_t.state_id = i
system.check_drift(X_t)
stats = system.get_stats()
assert stats['total_checks'] == 5
assert len(stats['recent_alerts']) <= 10
class TestIntegrationWithRealProfile:
"""Tests d'integration avec le vrai profil David."""
def test_load_real_profile(self):
"""Charger le vrai profil david_profile_declared.json."""
profile_path = Path(__file__).parent.parent / "david_profile_declared.json"
if not profile_path.exists():
pytest.skip("david_profile_declared.json not found")
x_ref = DavidReference.create_from_declared_profile(str(profile_path))
assert x_ref is not None
assert x_ref.state_id == -1
# Verifier que toutes les dimensions sont initialisees
for dim_name in DIMENSION_NAMES:
vec = getattr(x_ref, dim_name)
assert vec.shape == (EMBEDDING_DIM,)
assert np.isclose(np.linalg.norm(vec), 1.0)
if __name__ == "__main__":
pytest.main([__file__, "-v"])