Files

179 lines
6.5 KiB
Python
Raw Permalink Normal View History

"""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
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
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_exits_on_cors_wildcard_in_production(self):
"""validate_startup() should exit in production when CORS has wildcard."""
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"),
patch.object(settings, "cors_origins", ["*"]),
pytest.raises(SystemExit),
):
validate_startup(force=True)
def test_validate_startup_warns_cors_wildcard_in_dev(self):
"""validate_startup() should warn in dev when CORS has wildcard."""
from config import settings, validate_startup
with (
patch.object(settings, "timmy_env", "development"),
patch.object(settings, "cors_origins", ["*"]),
patch("config._startup_logger") as mock_logger,
):
validate_startup(force=True)
mock_logger.warning.assert_any_call(
"SEC: CORS_ORIGINS contains wildcard '*'"
"restrict to explicit origins before deploying to production."
)
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