Files
bigbrother/tests/test_engine.py
T
n0mad1k 3394c72814 Add main CLI entry point and full test suite
- bigbrother.py: Click CLI with start/stop/status/activate/deactivate/
  kill/config/selftest/modules commands plus Rich interactive menu.
  Module discovery scans modules/stealth/ for BaseModule subclasses.
  Supports --daemon mode with PID file and --passive-only flag.
- tests/conftest.py: Shared fixtures (tmp_dir, mock_config, mock_bus,
  mock_state, hardware_tier_override, project_root)
- tests/test_bus.py: 6 tests for EventBus pub/sub, filtering, wildcards
- tests/test_engine.py: 5 tests for Engine lifecycle, deps, tier, phase
- tests/test_crypto.py: 5 tests for AES-256-GCM, file encryption, KDF
- tests/test_resource.py: 7 tests for hardware detection, memory, disk
- tests/test_tool_manager.py: 6 tests for subprocess management
- tests/test_stealth_modules.py: 4 parametrized test classes covering
  all 12 stealth modules (import, inheritance, attributes, instantiation)
2026-03-18 09:48:23 -04:00

176 lines
5.5 KiB
Python

"""Tests for core.engine — Module lifecycle manager."""
import time
from unittest.mock import patch, MagicMock
import pytest
from core.engine import Engine, HARDWARE_TIERS, _topo_sort, ModuleEntry
from core.bus import EventBus
from core.state import StateManager
from modules.base import BaseModule
# ---------------------------------------------------------------------------
# Mock module for testing
# ---------------------------------------------------------------------------
class MockModule(BaseModule):
"""Minimal BaseModule subclass for testing."""
name = "mock_module"
module_type = "passive"
priority = 100
dependencies = []
requires_root = False
def start(self):
self._running = True
import os, time as _t
self._pid = os.getpid()
self._start_time = _t.time()
# Block to keep the process alive
import signal
signal.pause()
def stop(self):
self._running = False
def status(self):
return {"running": self._running, "pid": self._pid}
def configure(self, config):
self.config.update(config)
class MockActiveModule(BaseModule):
"""Active module for phase-gating tests."""
name = "mock_active"
module_type = "active"
priority = 200
dependencies = []
requires_root = False
def start(self):
self._running = True
def stop(self):
self._running = False
def status(self):
return {"running": self._running}
def configure(self, config):
pass
class MockDepModule(BaseModule):
"""Module that depends on mock_module."""
name = "mock_dep"
module_type = "passive"
priority = 150
dependencies = ["mock_module"]
requires_root = False
def start(self):
self._running = True
def stop(self):
self._running = False
def status(self):
return {"running": self._running}
def configure(self, config):
pass
class TestEngine:
def test_load_module(self, mock_bus, mock_state, mock_config):
"""Engine.register() adds a module to the registry."""
engine = Engine(bus=mock_bus, state=mock_state, config=mock_config)
engine.register(MockModule, config={"test": True})
assert "mock_module" in engine._modules
assert engine._modules["mock_module"].module_class is MockModule
assert engine._modules["mock_module"].config == {"test": True}
def test_start_stop_module(self, mock_bus, mock_state, mock_config):
"""Engine can start and stop a module process."""
engine = Engine(bus=mock_bus, state=mock_state, config=mock_config)
engine.register(MockModule, config=mock_config)
success = engine.start("mock_module")
assert success is True
entry = engine._modules["mock_module"]
assert entry.process is not None
assert entry.process.is_alive()
# Stop
engine.stop("mock_module")
time.sleep(0.5)
assert entry.process is None
def test_dependency_resolution(self, mock_bus, mock_state, mock_config):
"""_topo_sort orders modules by dependency (dependencies first)."""
engine = Engine(bus=mock_bus, state=mock_state, config=mock_config)
engine.register(MockDepModule)
engine.register(MockModule)
order = _topo_sort(engine._modules)
assert order.index("mock_module") < order.index("mock_dep")
def test_hardware_tier_enforcement(self, mock_bus, mock_state, mock_config, hardware_tier_override):
"""Pi Zero tier blocks more than max_passive modules."""
# Set platform to pi_zero directly in config
mock_config["device"]["platform"] = "pi_zero"
with hardware_tier_override("pi_zero"):
engine = Engine(bus=mock_bus, state=mock_state, config=mock_config)
# pi_zero allows max 4 passive modules
assert engine._tier == "pi_zero"
limits = HARDWARE_TIERS["pi_zero"]
assert limits["max_passive"] == 4
# Register 5 passive modules (create unique subclasses)
for i in range(5):
cls = type(f"MockPassive{i}", (MockModule,), {"name": f"passive_{i}"})
engine.register(cls, config=mock_config)
# Start modules — first 4 should succeed, 5th should be blocked
results = []
for i in range(5):
# Mock the process as alive for previously started ones
for name, entry in engine._modules.items():
if entry.process is not None:
entry.process = MagicMock()
entry.process.is_alive.return_value = True
results.append(engine.start(f"passive_{i}"))
# At least some should succeed and some should be blocked
started = sum(1 for r in results if r is True)
# The first 4 should start, 5th blocked by tier limit
assert started <= limits["max_passive"]
def test_engagement_phase_gating(self, mock_bus, mock_state, mock_config):
"""passive_only phase blocks active modules."""
mock_config["engagement_phase"]["mode"] = "passive_only"
engine = Engine(bus=mock_bus, state=mock_state, config=mock_config)
engine.register(MockActiveModule, config=mock_config)
success = engine.start("mock_active")
assert success is False
# Switch to active_allowed
engine.set_phase("active_allowed")
# Register again since it was already registered
success = engine.start("mock_active")
assert success is True
engine.stop("mock_active")