Files
bigbrother/tests/test_tool_manager.py
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

163 lines
4.8 KiB
Python

"""Tests for core.tool_manager — Subprocess lifecycle manager."""
import os
import time
import signal
from unittest.mock import MagicMock
import pytest
from core.tool_manager import ToolManager, ManagedTool
from core.bus import EventBus
class TestToolManager:
def test_register_tool(self, mock_bus):
"""register() adds a tool to the internal registry."""
tm = ToolManager(bus=mock_bus)
tm.register(
name="test_tool",
binary="/bin/echo",
args=["hello"],
max_restarts=2,
)
assert "test_tool" in tm._tools
tool = tm._tools["test_tool"]
assert tool.name == "test_tool"
assert tool.binary == "/bin/echo"
assert tool.args == ["hello"]
assert tool.max_restarts == 2
def test_start_stop_tool(self, mock_bus):
"""start() launches a subprocess and stop() terminates it."""
tm = ToolManager(bus=mock_bus)
tm.register(
name="sleeper",
binary="/bin/sleep",
args=["999"],
)
success = tm.start("sleeper")
assert success is True
status = tm.get_status("sleeper")
assert status["running"] is True
assert status["pid"] is not None
pid = status["pid"]
# Verify the process actually exists
try:
os.kill(pid, 0)
alive = True
except OSError:
alive = False
assert alive is True
# Stop
tm.stop("sleeper")
time.sleep(0.5)
status_after = tm.get_status("sleeper")
assert status_after["running"] is False
def test_restart_on_crash(self, mock_bus):
"""A tool that exits immediately triggers restart when monitor detects it."""
tm = ToolManager(bus=mock_bus)
# Register a tool that exits immediately (exit code 0)
tm.register(
name="crasher",
binary="/bin/false", # exits with code 1 immediately
args=[],
max_restarts=2,
)
success = tm.start("crasher")
# The start itself may succeed (process launched) even though it exits fast
# On some systems /bin/false exits so fast that poll() already shows it dead.
# Either way, the tool should have been created.
assert "crasher" in tm._tools
tool = tm._tools["crasher"]
# Wait briefly for the process to exit
time.sleep(0.5)
if tool.process is not None:
rc = tool.process.poll()
# /bin/false exits with 1
assert rc is not None # process already exited
def test_max_restarts(self, mock_bus):
"""After max_restarts, monitor gives up and leaves the tool stopped."""
tm = ToolManager(bus=mock_bus)
tm.register(
name="failbot",
binary="/bin/false",
args=[],
max_restarts=1,
)
# Start monitoring
tm.start_monitoring()
# Start the tool — it will exit immediately
tm.start("failbot")
# Wait for monitor to detect crash and exhaust restarts
# Monitor loop runs every 5 seconds, but we simulate faster
time.sleep(1.0)
tool = tm._tools["failbot"]
# Manually simulate what the monitor does
if tool.process and tool.process.poll() is not None:
tool.restart_count += 1
if tool.restart_count >= tool.max_restarts:
tool.process = None
tool.pid = None
# After exceeding max_restarts, process should be None
assert tool.restart_count >= tool.max_restarts
tm.stop_monitoring()
tm.stop_all()
def test_health_check(self, mock_bus):
"""Health check callback is stored and can be called."""
health_called = {"count": 0}
def my_health_check():
health_called["count"] += 1
return True
tm = ToolManager(bus=mock_bus)
tm.register(
name="healthy_tool",
binary="/bin/sleep",
args=["999"],
health_check=my_health_check,
)
tool = tm._tools["healthy_tool"]
assert tool.health_check is not None
# Call the health check directly
result = tool.health_check()
assert result is True
assert health_called["count"] == 1
# Start the tool and verify health check still works
tm.start("healthy_tool")
result2 = tool.health_check()
assert result2 is True
assert health_called["count"] == 2
tm.stop("healthy_tool")
def test_get_status_unregistered(self, mock_bus):
"""get_status() for an unregistered tool returns an error dict."""
tm = ToolManager(bus=mock_bus)
status = tm.get_status("nonexistent")
assert "error" in status