mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
Previously `core/config.py` defined `DEFAULT_CONFIG_DIR = Path.home() /
".openjarvis"` as a by-value module constant imported into ~45 modules, and 34
modules hardcoded `Path.home() / ".openjarvis"` directly. The installer honored
`OPENJARVIS_HOME` but the Python runtime ignored it, producing a split-brain
layout (some modules honored the override, the core config dir did not). Eval
dataset caches also scattered into `~/.cache/<benchmark>`.
This introduces a single env-aware resolver in `openjarvis/core/paths.py` and
routes every state/config/cache path through it. OpenJarvis now keeps ALL of
its state under ONE root, resolved in priority order:
1. $OPENJARVIS_HOME
2. $XDG_DATA_HOME/openjarvis (single nested dir, when XDG_DATA_HOME is set)
3. ~/.openjarvis (default — unchanged, so existing installs are
untouched and no data migration is required)
Implementation:
- New `core/paths.py`: get_config_dir / get_config_path / get_data_dir /
get_cache_dir, with a source-tree rejection guard (fails loudly per
REVIEW.md if the root resolves inside the repo).
- `core/config.py`: DEFAULT_CONFIG_DIR / DEFAULT_CONFIG_PATH are now resolved
via the env-aware resolver at import (real attributes, so existing
monkeypatch.setattr-based tests keep working). All dataclass field defaults
that pointed at ~/.openjarvis converted to default_factory so they honor the
override at instantiation.
- Routed all 34 hardcoders plus several string-literal escapees the original
audit missed: prompt_loader / description_loader (were OPENJARVIS_HOME-only,
no XDG), swebench_harness cache, tools/{memory,skill,user_profile}_manage
defaults, server trace.db fallbacks, doctor_cmd hints.
- spec_search storage/paths now delegates to the unified resolver (gains XDG);
its ConfigurationError is aliased to the core one.
- Eval dataset caches moved from ~/.cache/<name> to <root>/cache/<name>
(~/.cache/huggingface left alone — it is HF's own cache).
- Docs + installer comment + `jarvis config path` to show resolved dirs.
Read-only macOS connectors and OS service files (LaunchAgents/systemd) are
intentionally left untouched.
Fixes #462
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
6.9 KiB
Python
177 lines
6.9 KiB
Python
"""Tests for the env-aware OpenJarvis home-directory resolver (issue #462).
|
|
|
|
Covers the single-root consolidation: ``$OPENJARVIS_HOME`` >
|
|
``$XDG_DATA_HOME/openjarvis`` > ``~/.openjarvis``, backward compatibility
|
|
(no env => exactly ``~/.openjarvis``), and the source-tree rejection guard.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from openjarvis.core import paths
|
|
|
|
|
|
def _clear_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Remove every env var that influences home resolution."""
|
|
for var in (
|
|
"OPENJARVIS_HOME",
|
|
"XDG_DATA_HOME",
|
|
"XDG_CONFIG_HOME",
|
|
"XDG_CACHE_HOME",
|
|
):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
|
|
class TestGetConfigDir:
|
|
"""Precedence and backward compatibility of get_config_dir()."""
|
|
|
|
def test_default_when_unset_is_legacy_dir(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Backward-compat: with nothing set, the resolved dir is exactly the
|
|
# historical ~/.openjarvis so existing installs are untouched.
|
|
_clear_env(monkeypatch)
|
|
assert paths.get_config_dir() == (Path.home() / ".openjarvis").resolve()
|
|
|
|
def test_respects_openjarvis_home(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
custom = tmp_path / "oj"
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(custom))
|
|
assert paths.get_config_dir() == custom.resolve()
|
|
|
|
def test_respects_xdg_data_home(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
|
# Single nested 'openjarvis' dir under XDG_DATA_HOME.
|
|
assert paths.get_config_dir() == (tmp_path / "openjarvis").resolve()
|
|
|
|
def test_openjarvis_home_wins_over_xdg(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
oj = tmp_path / "oj_wins"
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(oj))
|
|
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg_loses"))
|
|
assert paths.get_config_dir() == oj.resolve()
|
|
|
|
def test_expands_user_in_openjarvis_home(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("OPENJARVIS_HOME", "~/relocated-oj")
|
|
assert paths.get_config_dir() == (Path.home() / "relocated-oj").resolve()
|
|
|
|
def test_returns_absolute_path(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(tmp_path / "rel"))
|
|
assert paths.get_config_dir().is_absolute()
|
|
|
|
|
|
class TestDerivedDirs:
|
|
"""config_path / data_dir / cache_dir all hang off the single root."""
|
|
|
|
def test_config_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(tmp_path / "oj"))
|
|
assert paths.get_config_path() == (tmp_path / "oj" / "config.toml").resolve()
|
|
|
|
def test_data_dir_equals_config_dir(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(tmp_path / "oj"))
|
|
assert paths.get_data_dir() == paths.get_config_dir()
|
|
|
|
def test_cache_dir_is_nested_cache(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(tmp_path / "oj"))
|
|
assert paths.get_cache_dir() == (tmp_path / "oj" / "cache").resolve()
|
|
|
|
def test_cache_dir_under_xdg(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
|
assert paths.get_cache_dir() == (tmp_path / "openjarvis" / "cache").resolve()
|
|
|
|
|
|
class TestSourceTreeRejection:
|
|
"""A home pointing inside the repo must fail loudly (REVIEW.md)."""
|
|
|
|
def test_rejects_path_inside_source_tree(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_clear_env(monkeypatch)
|
|
source_root = paths._find_source_root()
|
|
assert source_root is not None # We must be running inside the repo.
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(source_root / "junk_dir"))
|
|
with pytest.raises(paths.ConfigurationError, match="inside the source tree"):
|
|
paths.get_config_dir()
|
|
|
|
|
|
class TestLegacyConstantsHonorEnv:
|
|
"""The legacy DEFAULT_CONFIG_* names route through the env-aware resolver.
|
|
|
|
This is the exact split-brain bug from #462: the constant used to ignore
|
|
OPENJARVIS_HOME entirely. The constant is resolved once at import (the
|
|
install-script model, where the env is set before the process starts), and
|
|
every instance-level default goes through ``get_config_dir()`` so it honors
|
|
the override. ``DEFAULT_CONFIG_DIR`` stays a real attribute so existing
|
|
tests can ``monkeypatch.setattr`` it.
|
|
"""
|
|
|
|
def test_constant_matches_resolver_at_import(self) -> None:
|
|
from openjarvis.core import config
|
|
|
|
# The constant is the import-time resolution of the same function.
|
|
assert config.DEFAULT_CONFIG_DIR == paths.get_config_dir()
|
|
assert config.DEFAULT_CONFIG_PATH == paths.get_config_path()
|
|
|
|
def test_constant_is_a_real_settable_attribute(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Install/CLI tests monkeypatch this attribute directly; it must be a
|
|
# real module attribute (not __getattr__-only) for setattr/undo to work.
|
|
from openjarvis.core import config
|
|
|
|
monkeypatch.setattr(config, "DEFAULT_CONFIG_DIR", tmp_path / "patched")
|
|
assert config.DEFAULT_CONFIG_DIR == tmp_path / "patched"
|
|
|
|
def test_dataclass_defaults_reflect_env(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Config dataclass field defaults must resolve under the override at
|
|
# instantiation time, not freeze ~/.openjarvis at import.
|
|
_clear_env(monkeypatch)
|
|
from openjarvis.core.config import SessionConfig, StorageConfig
|
|
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(tmp_path / "oj"))
|
|
root = (tmp_path / "oj").resolve()
|
|
assert StorageConfig().db_path == str(root / "memory.db")
|
|
assert SessionConfig().db_path == str(root / "sessions.db")
|
|
|
|
def test_downstream_consumer_honors_env(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# End-to-end: a non-config subsystem (credentials) resolves under the
|
|
# custom root, proving the override is no longer split-brain.
|
|
_clear_env(monkeypatch)
|
|
from openjarvis.core import credentials
|
|
|
|
monkeypatch.setenv("OPENJARVIS_HOME", str(tmp_path / "oj"))
|
|
assert (
|
|
credentials._default_path()
|
|
== (tmp_path / "oj" / "credentials.toml").resolve()
|
|
)
|