- Fix TOCTOU race conditions by wrapping read-modify-write cycles under single-file locks in execution_state, portfolio_service, precheck_state, state_manager, and precheck_service. - Add missing test coverage (96 tests total): - test_review_service.py (15 tests) - test_check_api.py (6 tests) - test_external_gate.py main branches (+10 tests) - test_trade_execution.py new commands (+8 tests) - Unify all agent-consumed JSON messages to English. - Config-ize hardcoded values (volume filter, schema_version) via get_user_config with sensible defaults. - Add 1-hour TTL to exchange cache with force_new override. - Add ruff and mypy to dev dependencies; fix all type errors. - Add __all__ declarations to 11 service modules. - Sync README with new commands, config tuning docs, and PyPI badge. - Publish package as coinhunter==1.0.0 on PyPI with MIT license. - Deprecate coinhunter-cli==1.0.1 with runtime warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
"""Tests for runtime path resolution."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from coinhunter.runtime import RuntimePaths, ensure_runtime_dirs, get_runtime_paths, mask_secret
|
|
|
|
|
|
class TestGetRuntimePaths:
|
|
def test_defaults_point_to_home_dot_coinhunter(self):
|
|
paths = get_runtime_paths()
|
|
assert paths.root == Path.home() / ".coinhunter"
|
|
|
|
def test_respects_coinhunter_home_env(self, monkeypatch):
|
|
monkeypatch.setenv("COINHUNTER_HOME", "~/custom_ch")
|
|
paths = get_runtime_paths()
|
|
assert paths.root == Path.home() / "custom_ch"
|
|
|
|
def test_respects_hermes_home_env(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", "~/custom_hermes")
|
|
paths = get_runtime_paths()
|
|
assert paths.hermes_home == Path.home() / "custom_hermes"
|
|
assert paths.env_file == Path.home() / "custom_hermes" / ".env"
|
|
|
|
def test_returns_frozen_dataclass(self):
|
|
paths = get_runtime_paths()
|
|
assert isinstance(paths, RuntimePaths)
|
|
with pytest.raises(AttributeError):
|
|
paths.root = Path("/tmp")
|
|
|
|
def test_as_dict_returns_strings(self):
|
|
paths = get_runtime_paths()
|
|
d = paths.as_dict()
|
|
assert isinstance(d, dict)
|
|
assert all(isinstance(v, str) for v in d.values())
|
|
assert "root" in d
|
|
|
|
|
|
class TestEnsureRuntimeDirs:
|
|
def test_creates_directories(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("COINHUNTER_HOME", str(tmp_path))
|
|
paths = get_runtime_paths()
|
|
returned = ensure_runtime_dirs(paths)
|
|
assert returned.root.exists()
|
|
assert returned.state_dir.exists()
|
|
assert returned.logs_dir.exists()
|
|
assert returned.cache_dir.exists()
|
|
assert returned.reviews_dir.exists()
|
|
|
|
|
|
class TestMaskSecret:
|
|
def test_empty_string(self):
|
|
assert mask_secret("") == ""
|
|
|
|
def test_short_value(self):
|
|
assert mask_secret("ab") == "**"
|
|
|
|
def test_masks_all_but_tail(self):
|
|
assert mask_secret("supersecret", tail=4) == "*******cret"
|
|
|
|
def test_none_returns_empty(self):
|
|
assert mask_secret(None) == ""
|