2026-03-08 16:07:02 -04:00
|
|
|
"""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"
|
|
|
|
|
)
|
2026-03-08 16:07:02 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-03-19 15:29:26 -04:00
|
|
|
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."
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-08 16:07:02 -04:00
|
|
|
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
|