151 lines
5.2 KiB
Python
151 lines
5.2 KiB
Python
|
|
"""Tests for lazy singleton initialization (Ticket #2).
|
||
|
|
|
||
|
|
Verifies that importing modules does NOT trigger heavy side effects
|
||
|
|
(DB connections, HTTP calls, sys.exit, directory creation) and that
|
||
|
|
lazy getters return stable, resettable singleton instances.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import sys
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigLazyValidation:
|
||
|
|
"""config.py should not run startup validation at import time."""
|
||
|
|
|
||
|
|
def test_import_config_does_not_exit(self):
|
||
|
|
"""Importing config should never call sys.exit, even in production."""
|
||
|
|
with patch.dict("os.environ", {"TIMMY_ENV": "production"}, clear=False):
|
||
|
|
# Re-import config — should not sys.exit
|
||
|
|
if "config" in sys.modules:
|
||
|
|
mod = sys.modules["config"]
|
||
|
|
# validate_startup should exist as a callable
|
||
|
|
assert callable(
|
||
|
|
getattr(mod, "validate_startup", None)
|
||
|
|
), "config.validate_startup() must exist as an explicit init path"
|
||
|
|
|
||
|
|
def test_validate_startup_exits_on_missing_secrets_in_production(self):
|
||
|
|
"""validate_startup() should exit in production when secrets are missing."""
|
||
|
|
from config import settings, validate_startup
|
||
|
|
|
||
|
|
with (
|
||
|
|
patch.object(settings, "timmy_env", "production"),
|
||
|
|
patch.object(settings, "l402_hmac_secret", ""),
|
||
|
|
patch.object(settings, "l402_macaroon_secret", ""),
|
||
|
|
pytest.raises(SystemExit),
|
||
|
|
):
|
||
|
|
validate_startup(force=True)
|
||
|
|
|
||
|
|
def test_validate_startup_ok_with_secrets(self):
|
||
|
|
"""validate_startup() should not exit when secrets are set."""
|
||
|
|
from config import settings, validate_startup
|
||
|
|
|
||
|
|
with (
|
||
|
|
patch.object(settings, "timmy_env", "production"),
|
||
|
|
patch.object(settings, "l402_hmac_secret", "test-secret-hex-value-32"),
|
||
|
|
patch.object(settings, "l402_macaroon_secret", "test-macaroon-hex-value-32"),
|
||
|
|
):
|
||
|
|
# Should not raise
|
||
|
|
validate_startup(force=True)
|
||
|
|
|
||
|
|
def test_validate_startup_skips_in_test_mode(self):
|
||
|
|
"""validate_startup() should be a no-op in test mode."""
|
||
|
|
from config import validate_startup
|
||
|
|
|
||
|
|
# TIMMY_TEST_MODE=1 is set by conftest — should not raise
|
||
|
|
validate_startup()
|
||
|
|
|
||
|
|
|
||
|
|
class TestSparkEngineLazy:
|
||
|
|
"""spark.engine should not create the engine at import time."""
|
||
|
|
|
||
|
|
def test_get_spark_engine_returns_instance(self):
|
||
|
|
"""get_spark_engine() should return a SparkEngine."""
|
||
|
|
from spark.engine import SparkEngine, get_spark_engine
|
||
|
|
|
||
|
|
engine = get_spark_engine()
|
||
|
|
assert isinstance(engine, SparkEngine)
|
||
|
|
|
||
|
|
def test_get_spark_engine_is_singleton(self):
|
||
|
|
"""Repeated calls return the same instance."""
|
||
|
|
from spark.engine import get_spark_engine
|
||
|
|
|
||
|
|
a = get_spark_engine()
|
||
|
|
b = get_spark_engine()
|
||
|
|
assert a is b
|
||
|
|
|
||
|
|
def test_get_spark_engine_reset(self):
|
||
|
|
"""reset_spark_engine() allows re-initialization for tests."""
|
||
|
|
from spark.engine import get_spark_engine, reset_spark_engine
|
||
|
|
|
||
|
|
a = get_spark_engine()
|
||
|
|
reset_spark_engine()
|
||
|
|
b = get_spark_engine()
|
||
|
|
assert a is not b
|
||
|
|
|
||
|
|
def test_spark_engine_backward_compat(self):
|
||
|
|
"""spark_engine module-level name still works via get_spark_engine."""
|
||
|
|
from spark.engine import spark_engine
|
||
|
|
|
||
|
|
assert spark_engine is not None
|
||
|
|
|
||
|
|
|
||
|
|
class TestMemorySystemLazy:
|
||
|
|
"""timmy.memory_system should not create the system at import time."""
|
||
|
|
|
||
|
|
def test_get_memory_system_returns_instance(self):
|
||
|
|
"""get_memory_system() should return a MemorySystem."""
|
||
|
|
from timmy.memory_system import MemorySystem, get_memory_system
|
||
|
|
|
||
|
|
ms = get_memory_system()
|
||
|
|
assert isinstance(ms, MemorySystem)
|
||
|
|
|
||
|
|
def test_get_memory_system_is_singleton(self):
|
||
|
|
"""Repeated calls return the same instance."""
|
||
|
|
from timmy.memory_system import get_memory_system
|
||
|
|
|
||
|
|
a = get_memory_system()
|
||
|
|
b = get_memory_system()
|
||
|
|
assert a is b
|
||
|
|
|
||
|
|
def test_get_memory_system_reset(self):
|
||
|
|
"""reset_memory_system() allows re-initialization for tests."""
|
||
|
|
from timmy.memory_system import get_memory_system, reset_memory_system
|
||
|
|
|
||
|
|
a = get_memory_system()
|
||
|
|
reset_memory_system()
|
||
|
|
b = get_memory_system()
|
||
|
|
assert a is not b
|
||
|
|
|
||
|
|
def test_memory_system_backward_compat(self):
|
||
|
|
"""memory_system module-level name still works."""
|
||
|
|
from timmy.memory_system import memory_system
|
||
|
|
|
||
|
|
assert memory_system is not None
|
||
|
|
|
||
|
|
|
||
|
|
class TestEventBusLazy:
|
||
|
|
"""infrastructure.events.bus should use lazy initialization."""
|
||
|
|
|
||
|
|
def test_get_event_bus_returns_instance(self):
|
||
|
|
"""get_event_bus() should return an EventBus."""
|
||
|
|
from infrastructure.events.bus import EventBus, get_event_bus
|
||
|
|
|
||
|
|
bus = get_event_bus()
|
||
|
|
assert isinstance(bus, EventBus)
|
||
|
|
|
||
|
|
def test_get_event_bus_is_singleton(self):
|
||
|
|
"""Repeated calls return the same instance."""
|
||
|
|
from infrastructure.events.bus import get_event_bus
|
||
|
|
|
||
|
|
a = get_event_bus()
|
||
|
|
b = get_event_bus()
|
||
|
|
assert a is b
|
||
|
|
|
||
|
|
def test_event_bus_backward_compat(self):
|
||
|
|
"""event_bus module-level name still works."""
|
||
|
|
from infrastructure.events.bus import event_bus
|
||
|
|
|
||
|
|
assert event_bus is not None
|