fix(paths): module-level default init so subprocess imports don't crash
This commit is contained in:
@@ -68,6 +68,39 @@ _PATHS_CONFIG: Optional[PathsConfig] = None
|
||||
_PATHS_LOCK = threading.RLock()
|
||||
|
||||
|
||||
def _default_paths_config() -> PathsConfig:
|
||||
"""Build the default PathsConfig (no [paths] overrides, just defaults).
|
||||
Called once at module load to ensure _PATHS_CONFIG is never None for
|
||||
callers that don't explicitly initialize (e.g., subprocess imports).
|
||||
[C: src/paths.py:initialize_paths, src/paths.py:_module_init_default]"""
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
config_path = root_dir / "config.toml"
|
||||
cfg = PathsConfig(
|
||||
config_path = config_path,
|
||||
presets = root_dir / "presets.toml",
|
||||
tool_presets = root_dir / "tool_presets.toml",
|
||||
personas = root_dir / "personas.toml",
|
||||
themes = root_dir / "themes",
|
||||
workspace_profiles = root_dir / "workspace_profiles.toml",
|
||||
credentials = root_dir / "credentials.toml",
|
||||
logs_dir = root_dir / "logs" / "sessions",
|
||||
scripts_dir = root_dir / "scripts" / "generated",
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
||||
def _module_init_default() -> None:
|
||||
"""Initialize _PATHS_CONFIG with defaults at module load.
|
||||
Idempotent. Subsequent calls to initialize_paths(<custom>) override this.
|
||||
[C: src/paths.py:initialize_paths, src/paths.py:reset_paths]"""
|
||||
global _PATHS_CONFIG
|
||||
if _PATHS_CONFIG is None:
|
||||
_PATHS_CONFIG = _default_paths_config()
|
||||
|
||||
|
||||
_module_init_default()
|
||||
|
||||
|
||||
def _resolve_path(env_var: str, config_key: str, default: Path, config_path: Path) -> Path:
|
||||
"""Internal: resolve one path from env var -> config [paths] -> default.
|
||||
Called only from initialize_paths(). Not thread-safe; caller holds lock."""
|
||||
|
||||
@@ -181,14 +181,36 @@ def test_paths_runtime_refresh_atomic_swap(tmp_path) -> None:
|
||||
|
||||
|
||||
def test_paths_uninitialized_raises(tmp_path) -> None:
|
||||
"""Calling a path getter before initialize_paths() raises RuntimeError
|
||||
(not silent fallback). This is the contract that enforces explicit init.
|
||||
"""After explicit paths.reset_paths() (i.e., user CLEARED the singleton
|
||||
after a previous init), a getter raises RuntimeError. This is the
|
||||
"bad programmer" detection — once cleared, you must re-init.
|
||||
[C: src/paths.py:_cfg]"""
|
||||
from src import paths
|
||||
paths.initialize_paths(tmp_path / "dummy.toml")
|
||||
paths.reset_paths()
|
||||
with pytest.raises(RuntimeError, match="not initialized"):
|
||||
paths.get_logs_dir()
|
||||
paths.reset_paths()
|
||||
|
||||
|
||||
def test_paths_module_load_initializes_defaults(tmp_path) -> None:
|
||||
"""src/paths.py initializes _PATHS_CONFIG with defaults at module load.
|
||||
This means subprocess imports that don't go through conftest.py (e.g.,
|
||||
_run_in_subprocess tests) still have valid paths for any src/* module
|
||||
that triggers a paths getter at import time (e.g., theme_2.load_themes).
|
||||
[C: src/paths.py:_module_init_default]"""
|
||||
import importlib
|
||||
import src.paths as paths_module
|
||||
# Reload to simulate fresh module load in subprocess
|
||||
importlib.reload(paths_module)
|
||||
# After module reload, defaults should be set
|
||||
assert paths_module._PATHS_CONFIG is not None, (
|
||||
"src.paths must initialize _PATHS_CONFIG at module load "
|
||||
"so subprocess imports don't trigger 'paths not initialized' errors."
|
||||
)
|
||||
default_logs = paths_module._PATHS_CONFIG.logs_dir
|
||||
assert default_logs.name == "sessions", (
|
||||
f"default logs_dir should end in 'sessions'; got {default_logs}"
|
||||
)
|
||||
|
||||
|
||||
def test_sloppy_py_parses_config_flag() -> None:
|
||||
|
||||
Reference in New Issue
Block a user