darkplex-core/tests/test_new_modules.py
Claudia 47f9703e3b
All checks were successful
Tests / test (push) Successful in 3s
feat: port needs, alert, summarize, anomaly, predict, monitor modules
2026-02-09 16:20:22 +01:00

679 lines
22 KiB
Python

#!/usr/bin/env python3
"""Tests for new cortex modules: learn, context, tracker, sentinel.
Run with: CORTEX_HOME=~/clawd python3 -m pytest tests/test_new_modules.py -v
"""
import json
import os
import sys
import tempfile
from pathlib import Path
from datetime import datetime, timedelta
from unittest.mock import patch, MagicMock
import pytest
# Set up CORTEX_HOME before importing modules
os.environ.setdefault('CORTEX_HOME', os.path.expanduser('~/clawd'))
os.environ.setdefault('CORTEX_MEMORY_DIR', os.path.expanduser('~/clawd/memory'))
from cortex import learn, context, tracker, sentinel
from cortex.config import cortex_home, memory_dir
# --- Fixtures ---
@pytest.fixture
def temp_cortex_home(tmp_path):
"""Temporary CORTEX_HOME for isolated tests."""
old_home = os.environ.get('CORTEX_HOME')
old_memory = os.environ.get('CORTEX_MEMORY_DIR')
os.environ['CORTEX_HOME'] = str(tmp_path)
os.environ['CORTEX_MEMORY_DIR'] = str(tmp_path / 'memory')
yield tmp_path
if old_home:
os.environ['CORTEX_HOME'] = old_home
else:
os.environ.pop('CORTEX_HOME', None)
if old_memory:
os.environ['CORTEX_MEMORY_DIR'] = old_memory
else:
os.environ.pop('CORTEX_MEMORY_DIR', None)
@pytest.fixture
def sample_events():
"""Sample NATS events for testing."""
now = int(datetime.now().timestamp() * 1000)
return [
{
'id': 'evt-001',
'timestamp': now - 3600000, # 1 hour ago
'type': 'conversation_message_in',
'payload': {
'data': {'text': 'Das war mega! Super gemacht 👍'}
}
},
{
'id': 'evt-002',
'timestamp': now - 7200000, # 2 hours ago
'type': 'conversation_message_out',
'payload': {
'data': {'text': 'Ich werde das TypeScript Projekt morgen fertigstellen.'}
}
},
{
'id': 'evt-003',
'timestamp': now - 10800000, # 3 hours ago
'type': 'tool_call',
'payload': {
'data': {'name': 'Read', 'text': 'Reading file...'}
}
},
{
'id': 'evt-004',
'timestamp': now - 14400000, # 4 hours ago
'type': 'conversation_message_in',
'payload': {
'data': {'text': 'Nein, ich meinte eigentlich Python, nicht JavaScript.'}
}
},
{
'id': 'evt-005',
'timestamp': now - 18000000, # 5 hours ago
'type': 'error',
'payload': {
'data': {'text': 'Error: Module not found in Node.js project'}
}
},
]
@pytest.fixture
def sample_alerts():
"""Sample security alerts for testing."""
return [
{
'id': 'alert-001',
'source': 'bleepingcomputer',
'category': 'security-news',
'title': 'Critical OpenSSH vulnerability allows remote code execution',
'link': 'https://example.com/openssh-vuln',
'summary': 'A critical RCE vulnerability in OpenSSH...',
'severity': 'critical',
'relevant': True,
},
{
'id': 'alert-002',
'source': 'hackernews',
'category': 'security-news',
'title': 'New Docker container escape vulnerability discovered',
'link': 'https://example.com/docker-escape',
'summary': 'Researchers found a container escape bug in Docker...',
'severity': 'high',
'relevant': True,
},
{
'id': 'alert-003',
'source': 'schneier',
'category': 'security-news',
'title': 'Thoughts on Password Managers',
'link': 'https://example.com/password-mgr',
'summary': 'Discussion about password manager security...',
'severity': 'info',
'relevant': False,
},
]
# --- Learn Module Tests ---
class TestLearn:
"""Tests for cortex.learn module."""
def test_parse_duration_hours(self):
"""Test duration parsing for hours."""
delta = learn.parse_duration('24h')
assert delta == timedelta(hours=24)
def test_parse_duration_days(self):
"""Test duration parsing for days."""
delta = learn.parse_duration('7d')
assert delta == timedelta(days=7)
def test_parse_duration_minutes(self):
"""Test duration parsing for minutes."""
delta = learn.parse_duration('30m')
assert delta == timedelta(minutes=30)
def test_parse_duration_invalid(self):
"""Test invalid duration format."""
with pytest.raises(ValueError):
learn.parse_duration('invalid')
def test_extract_text_from_payload(self):
"""Test text extraction from various event formats."""
event = {'payload': {'data': {'text': 'Hello world'}}}
assert learn.extract_text(event) == 'Hello world'
def test_extract_text_from_content(self):
"""Test text extraction from content field."""
event = {'payload': {'content': 'Test content'}}
assert learn.extract_text(event) == 'Test content'
def test_extract_signals_positive(self, sample_events):
"""Test extraction of positive signals."""
signals = learn.extract_signals(sample_events)
assert len(signals['positive']) >= 1
# Should detect "mega" and "Super gemacht"
def test_extract_signals_corrections(self, sample_events):
"""Test extraction of correction signals."""
signals = learn.extract_signals(sample_events)
assert len(signals['corrections']) >= 1
# Should detect "Nein, ich meinte eigentlich"
def test_extract_signals_skill_gaps(self, sample_events):
"""Test extraction of skill gaps."""
signals = learn.extract_signals(sample_events)
assert 'node' in signals['skill_gaps'] or 'nodejs' in signals['skill_gaps'].keys()
def test_derive_preferences(self, sample_events):
"""Test preference derivation."""
signals = learn.extract_signals(sample_events)
prefs = learn.derive_preferences(signals, {})
assert 'lastAnalysis' in prefs
assert 'signalCounts' in prefs
assert 'explicit' in prefs
def test_run_learning_cycle(self, temp_cortex_home, sample_events):
"""Test full learning cycle."""
results = learn.run_learning_cycle(sample_events, 'test')
assert 'preferences' in results
assert 'behaviors' in results
assert 'topics' in results
assert 'skill_gaps' in results
# Check files were created
ldir = temp_cortex_home / 'learning' / 'test'
assert (ldir / 'preferences.json').exists()
assert (ldir / 'behaviors.json').exists()
assert (ldir / 'learning-context.md').exists()
# --- Context Module Tests ---
class TestContext:
"""Tests for cortex.context module."""
def test_extract_text(self):
"""Test text extraction from events."""
event = {'payload': {'data': {'text': 'Test message'}}}
text = context.extract_text(event)
assert text == 'Test message'
def test_analyze_events_topics(self, sample_events):
"""Test topic detection in events."""
patterns = context.analyze_events(sample_events)
# Should have detected topics based on keywords
assert isinstance(patterns['topics'], dict)
def test_analyze_events_tools(self, sample_events):
"""Test tool tracking."""
patterns = context.analyze_events(sample_events)
assert 'Read' in patterns['tools']
def test_analyze_events_languages(self, sample_events):
"""Test language detection."""
patterns = context.analyze_events(sample_events)
# Should detect German (more German words in samples)
assert patterns['languages']['de'] > 0
def test_generate_context_md(self, sample_events):
"""Test markdown generation."""
patterns = context.analyze_events(sample_events)
md = context.generate_context_md(patterns, len(sample_events), None)
assert '# Learning Context' in md
assert 'User Preferences' in md
assert 'Most Used Tools' in md
def test_run_context_generation(self, temp_cortex_home, sample_events):
"""Test full context generation."""
patterns = context.run_context_generation(sample_events, agent='test')
# Check output file
output = temp_cortex_home / 'learning' / 'test' / 'learning-context.md'
assert output.exists()
content = output.read_text()
assert '# Learning Context' in content
# --- Tracker Module Tests ---
class TestTracker:
"""Tests for cortex.tracker module."""
def test_clean_text(self):
"""Test text cleaning."""
text = "Hello [timestamp] **bold** https://example.com"
clean = tracker.clean_text(text)
assert '[timestamp]' not in clean
assert '**' not in clean
assert 'https://' not in clean
def test_extract_text(self):
"""Test text extraction from events."""
event = {'payload': {'text_preview': [{'text': 'Test'}]}}
text = tracker.extract_text(event)
assert text == 'Test'
def test_load_save_db(self, temp_cortex_home):
"""Test database load/save."""
db = tracker.load_db()
assert 'commitments' in db
assert 'claims' in db
assert 'processedIds' in db
# Add some data
db['commitments'].append({'id': 'test-001', 'what': 'Test commitment'})
db['processedIds'].add('evt-001')
tracker.save_db(db)
# Reload and verify
db2 = tracker.load_db()
assert len(db2['commitments']) == 1
assert 'evt-001' in db2['processedIds']
@patch('cortex.tracker.query_llm')
def test_analyze_message_no_llm(self, mock_llm):
"""Test message analysis fallback."""
mock_llm.return_value = ''
result = tracker.analyze_message('Short text', [])
assert result is None # Too short after cleaning
def test_cmd_list(self, temp_cortex_home, capsys):
"""Test list command output."""
# Create test data
db = tracker.load_db()
db['commitments'].append({
'id': 'commit-test',
'what': 'Test commitment',
'who': 'user',
'date': '2024-01-01',
'status': 'open',
})
tracker.save_db(db)
tracker.cmd_list()
captured = capsys.readouterr()
assert 'Test commitment' in captured.out
# --- Sentinel Module Tests ---
class TestSentinel:
"""Tests for cortex.sentinel module."""
def test_init_db(self, temp_cortex_home):
"""Test database initialization."""
sentinel.init_db()
db_path = temp_cortex_home / 'sentinel' / 'sentinel.db'
assert db_path.exists()
def test_add_alert_new(self, temp_cortex_home, sample_alerts):
"""Test adding new alerts."""
sentinel.init_db()
alert = sample_alerts[0]
is_new = sentinel.add_alert(alert)
assert is_new is True
# Adding same alert again should return False
is_new2 = sentinel.add_alert(alert)
assert is_new2 is False
def test_get_stats(self, temp_cortex_home, sample_alerts):
"""Test statistics retrieval."""
sentinel.init_db()
for alert in sample_alerts:
sentinel.add_alert(alert)
stats = sentinel.get_stats()
assert stats['total_alerts'] == 3
assert 'by_severity' in stats
assert stats['by_severity'].get('critical', 0) == 1
def test_check_inventory_match(self):
"""Test inventory matching."""
text = "Critical vulnerability in OpenSSH and Docker"
matches = sentinel.check_inventory_match(text)
# Should match OpenSSH and Docker
names = [m['name'] for m in matches]
assert 'OpenSSH' in names
assert 'Docker' in names
def test_analyze_matches(self, sample_alerts):
"""Test alert analysis for matches."""
result = sentinel.analyze_matches(sample_alerts)
assert 'relevant_alerts' in result
assert 'critical_relevant' in result
assert 'category_breakdown' in result
def test_generate_report(self, temp_cortex_home, sample_alerts):
"""Test report generation."""
sentinel.init_db()
for alert in sample_alerts:
sentinel.add_alert(alert)
data = sentinel.analyze_matches(sample_alerts)
report = sentinel.generate_report(data, use_llm=False)
assert '# 🔒 Security Sentinel Report' in report
assert 'Database Stats' in report
# --- Integration Tests ---
class TestIntegration:
"""Integration tests for module interactions."""
def test_cli_learn_help(self):
"""Test learn command help."""
import subprocess
result = subprocess.run(
['python3', '-m', 'cortex.learn', '--help'],
capture_output=True, text=True
)
assert 'preferences' in result.stdout.lower() or result.returncode == 0
def test_cli_context_help(self):
"""Test context command help."""
import subprocess
result = subprocess.run(
['python3', '-m', 'cortex.context', '--help'],
capture_output=True, text=True
)
assert 'events' in result.stdout.lower() or result.returncode == 0
def test_cli_tracker_help(self):
"""Test tracker command help."""
import subprocess
result = subprocess.run(
['python3', '-m', 'cortex.tracker', '--help'],
capture_output=True, text=True
)
assert 'scan' in result.stdout.lower() or result.returncode == 0
def test_cli_sentinel_help(self):
"""Test sentinel command help."""
import subprocess
result = subprocess.run(
['python3', '-m', 'cortex.sentinel', '--help'],
capture_output=True, text=True
)
assert 'scan' in result.stdout.lower() or result.returncode == 0
if __name__ == '__main__':
pytest.main([__file__, '-v'])
# --- Needs Module Tests ---
class TestNeeds:
"""Tests for cortex.needs module."""
def test_import(self):
from cortex import needs
assert hasattr(needs, 'assess_wellbeing')
assert hasattr(needs, 'format_status')
def test_classify(self):
from cortex.needs import _classify
assert _classify(0.9) == "satisfied"
assert _classify(0.5) == "low"
assert _classify(0.1) == "critical"
def test_assess_wellbeing(self, temp_cortex_home):
from cortex.needs import assess_wellbeing
wb = assess_wellbeing()
assert 0.0 <= wb.overall <= 1.0
assert wb.status in ("thriving", "okay", "struggling", "critical")
assert "context" in wb.needs
assert "health" in wb.needs
def test_save_and_load(self, temp_cortex_home):
from cortex.needs import assess_wellbeing, save_wellbeing, wellbeing_file
wb = assess_wellbeing()
save_wellbeing(wb)
wf = wellbeing_file()
assert wf.exists()
data = json.loads(wf.read_text())
assert "overall" in data
assert "history" in data
def test_format_status(self):
from cortex.needs import assess_wellbeing, format_status
wb = assess_wellbeing()
output = format_status(wb)
assert "Wellbeing" in output
# --- Alert Module Tests ---
class TestAlert:
"""Tests for cortex.alert module."""
def test_import(self):
from cortex import alert
assert hasattr(alert, 'Alert')
assert hasattr(alert, 'format_dashboard')
def test_alert_creation(self):
from cortex.alert import Alert
a = Alert("test", "critical", "Test alert")
assert a.source == "test"
assert a.level == "critical"
d = a.to_dict()
assert d["source"] == "test"
def test_format_dashboard_empty(self):
from cortex.alert import format_dashboard
output = format_dashboard([])
assert "All clear" in output
def test_format_dashboard_with_alerts(self):
from cortex.alert import Alert, format_dashboard
alerts = [Alert("test", "critical", "Something broke")]
output = format_dashboard(alerts)
assert "Critical" in output
assert "Something broke" in output
# --- Summarize Module Tests ---
class TestSummarize:
"""Tests for cortex.summarize module."""
def test_import(self):
from cortex import summarize
assert hasattr(summarize, 'get_day_events')
assert hasattr(summarize, 'warm_dir')
def test_warm_dir(self, temp_cortex_home):
from cortex.summarize import warm_dir
wd = warm_dir()
assert "brain" in str(wd)
assert "warm" in str(wd)
# --- Anomaly Module Tests ---
class TestAnomaly:
"""Tests for cortex.anomaly module."""
def test_import(self):
from cortex import anomaly
assert hasattr(anomaly, 'detect_anomalies')
def test_detect_no_events(self):
from cortex.anomaly import detect_anomalies
assert detect_anomalies([]) == []
def test_detect_few_events(self):
from cortex.anomaly import detect_anomalies
events = [{"type": "msg", "text": "hi", "tool": "", "isError": False}] * 5
assert detect_anomalies(events) == []
def test_detect_error_spike(self):
from cortex.anomaly import detect_anomalies
events = [{"type": "error", "text": "error occurred", "tool": "", "isError": True}] * 15
anomalies = detect_anomalies(events)
assert any(a["type"] == "error_spike" for a in anomalies)
def test_format_report_clean(self):
from cortex.anomaly import format_report
assert "No anomalies" in format_report([])
def test_state_file(self, temp_cortex_home):
from cortex.anomaly import state_file, _load_state, _save_state
state = _load_state()
assert "lastCheck" in state
_save_state(state)
assert state_file().exists()
# --- Predict Module Tests ---
class TestPredict:
"""Tests for cortex.predict module."""
def test_import(self):
from cortex import predict
assert hasattr(predict, 'predict_actions')
assert hasattr(predict, 'learn_patterns')
def test_categorize_activity(self):
from cortex.predict import categorize_activity
assert categorize_activity({"text": "check email inbox", "tool": "", "type": "msg"}) == "email"
assert categorize_activity({"text": "git commit", "tool": "", "type": "msg"}) == "git"
assert categorize_activity({"text": "something", "tool": "exec", "type": "tool"}) == "shell"
def test_load_empty_patterns(self, temp_cortex_home):
from cortex.predict import _load_patterns
p = _load_patterns()
assert "timePatterns" in p
assert "sequences" in p
def test_learn_patterns(self):
from cortex.predict import learn_patterns
from datetime import datetime
events = [
{"time": datetime(2026, 1, 1, 9, 0), "type": "msg", "text": "check email", "tool": "", "agent": "main"},
{"time": datetime(2026, 1, 1, 9, 5), "type": "msg", "text": "git push", "tool": "", "agent": "main"},
]
patterns = learn_patterns(events)
assert len(patterns["timePatterns"]) > 0
def test_predict_empty(self):
from cortex.predict import predict_actions
preds = predict_actions({"timePatterns": {}, "sequences": {}})
assert preds == []
# --- Monitor Module Tests ---
class TestMonitor:
"""Tests for cortex.monitor module."""
def test_import(self):
from cortex import monitor
assert hasattr(monitor, 'get_dashboard')
assert hasattr(monitor, 'AGENTS')
def test_agents_config(self):
from cortex.monitor import AGENTS
assert "main" in AGENTS
assert "stream" in AGENTS["main"]
def test_format_bytes(self):
from cortex.monitor import format_bytes
assert "KB" in format_bytes(2048)
assert "MB" in format_bytes(2 * 1024 * 1024)
assert "B" in format_bytes(100)
def test_format_age_none(self):
from cortex.monitor import format_age
assert format_age(None) == "never"
assert format_age("0001-01-01T00:00:00Z") == "never"
def test_format_dashboard(self):
from cortex.monitor import format_dashboard
data = [{
"agent_id": "main", "name": "Claudia", "emoji": "🛡️",
"stream": "openclaw-events", "messages": 100, "bytes": 1024,
"last_ts": None, "msg_in": 50, "msg_out": 40, "tool_calls": 10, "lifecycle": 0,
}]
output = format_dashboard(data)
assert "Claudia" in output
assert "NEURAL MONITOR" in output
# --- CLI Integration Tests for New Modules ---
class TestNewModulesCLI:
"""CLI integration tests for the 6 new modules."""
def test_cli_needs_help(self):
import subprocess
r = subprocess.run(['python3', '-m', 'cortex.needs', '--help'],
capture_output=True, text=True)
assert r.returncode == 0
def test_cli_alert_help(self):
import subprocess
r = subprocess.run(['python3', '-m', 'cortex.alert', '--help'],
capture_output=True, text=True)
assert r.returncode == 0
def test_cli_summarize_help(self):
import subprocess
r = subprocess.run(['python3', '-m', 'cortex.summarize', '--help'],
capture_output=True, text=True)
assert r.returncode == 0
def test_cli_anomaly_help(self):
import subprocess
r = subprocess.run(['python3', '-m', 'cortex.anomaly', '--help'],
capture_output=True, text=True)
assert r.returncode == 0
def test_cli_predict_help(self):
import subprocess
r = subprocess.run(['python3', '-m', 'cortex.predict', '--help'],
capture_output=True, text=True)
assert r.returncode == 0
def test_cli_monitor_help(self):
import subprocess
r = subprocess.run(['python3', '-m', 'cortex.monitor', '--help'],
capture_output=True, text=True)
assert r.returncode == 0