feat(config): config 디렉터리 기반 설정 관리 (--config-path/--env-file, trex-orb-v1 정합, BAT-026)
REQ-048/010 change-req. `--config` 를 이름으로 해석 + `--config-path <dir>` (기본 ./config) + resolve_config_toml(슬래시 포함 값은 하위호환 명시 경로). `.env` 를 config-dir 결정적 해석(--env-file 우선, 없으면 <config_dir>/.env, CWD 무관) + config/.env.example + .gitignore config/* 허용목록. 운영자 CLI 도 --config-path/--env-file 동일 적용. from_env() legacy 루트 .env 폴백 유지로 완전 하위호환. 단위 23 + 전체 375 passed, ruff clean, 라이브 스모크(name·path 동일). REQ-048/010 → Implemented. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""config-dir resolution + config-dir/.env loading (REQ-048/010 CHG-20260609-01)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.runtime.config import _load_dotenv, resolve_config_toml
|
||||
|
||||
_K = "TREX_TEST_RESOLVE_KEY"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean(monkeypatch):
|
||||
monkeypatch.delenv(_K, raising=False)
|
||||
|
||||
|
||||
# --- resolve_config_toml (REQ-048 AC-7/8) ---------------------------------- #
|
||||
def test_resolve_name_default_dir():
|
||||
assert resolve_config_toml("test") == os.path.join("config", "test.toml")
|
||||
|
||||
|
||||
def test_resolve_appends_toml():
|
||||
assert resolve_config_toml("test.toml") == os.path.join("config", "test.toml")
|
||||
|
||||
|
||||
def test_resolve_with_config_path():
|
||||
assert resolve_config_toml("prod", "/etc/trex") == os.path.join("/etc/trex", "prod.toml")
|
||||
|
||||
|
||||
def test_resolve_path_form_passthrough():
|
||||
# AC-8: a slash-bearing value is an explicit path (backward-compat).
|
||||
assert resolve_config_toml("config/test.toml") == "config/test.toml"
|
||||
assert resolve_config_toml("config/test.toml", "/etc/trex") == "config/test.toml"
|
||||
|
||||
|
||||
# --- _load_dotenv config-dir resolution (REQ-010 AC-6/7) ------------------- #
|
||||
def test_dotenv_loaded_from_config_dir_cwd_independent(tmp_path, monkeypatch):
|
||||
cfgdir = tmp_path / "cfg"
|
||||
cfgdir.mkdir()
|
||||
(cfgdir / ".env").write_text(f"{_K}=from_config_dir\n")
|
||||
toml = str(cfgdir / "inst.toml")
|
||||
# chdir somewhere with no .env to prove CWD-independence.
|
||||
other = tmp_path / "elsewhere"
|
||||
other.mkdir()
|
||||
monkeypatch.chdir(other)
|
||||
_load_dotenv(toml)
|
||||
assert os.environ[_K] == "from_config_dir"
|
||||
|
||||
|
||||
def test_dotenv_env_file_wins(tmp_path, monkeypatch):
|
||||
cfgdir = tmp_path / "cfg"
|
||||
cfgdir.mkdir()
|
||||
(cfgdir / ".env").write_text(f"{_K}=from_config_dir\n")
|
||||
explicit = tmp_path / "explicit.env"
|
||||
explicit.write_text(f"{_K}=from_env_file\n")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_load_dotenv(str(cfgdir / "inst.toml"), env_file=str(explicit))
|
||||
assert os.environ[_K] == "from_env_file"
|
||||
|
||||
|
||||
def test_dotenv_os_env_takes_precedence(tmp_path, monkeypatch):
|
||||
cfgdir = tmp_path / "cfg"
|
||||
cfgdir.mkdir()
|
||||
(cfgdir / ".env").write_text(f"{_K}=from_config_dir\n")
|
||||
monkeypatch.setenv(_K, "from_os_env")
|
||||
_load_dotenv(str(cfgdir / "inst.toml"))
|
||||
assert os.environ[_K] == "from_os_env" # override=False
|
||||
|
||||
|
||||
def test_dotenv_absent_is_noop(tmp_path):
|
||||
# config dir has no .env -> no crash, key stays unset.
|
||||
_load_dotenv(str(tmp_path / "inst.toml"))
|
||||
assert _K not in os.environ
|
||||
|
||||
|
||||
# --- CLI --config-path instance routing (REQ-048 AC-9) --------------------- #
|
||||
def test_cli_resolve_instance_honors_config_path(tmp_path):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from trex_core.runtime.cli import _resolve_instance
|
||||
|
||||
cfgdir = tmp_path / "cfg"
|
||||
cfgdir.mkdir()
|
||||
(cfgdir / "inst.toml").write_text('instance_key = "inst-xyz"\n')
|
||||
args = SimpleNamespace(instance=None, config="inst", config_path=str(cfgdir))
|
||||
assert _resolve_instance(args) == "inst-xyz"
|
||||
@@ -83,7 +83,9 @@ def test_config_required_cli_exits_nonzero():
|
||||
|
||||
|
||||
def test_gitignore_excludes_instance_toml():
|
||||
"""AC-5: .gitignore excludes instance TOML but keeps the example."""
|
||||
"""AC-5 (CHG-20260609-01): .gitignore ignores config/* but keeps example
|
||||
templates (*.example.toml covers instance.example.toml) and .env.example."""
|
||||
gi = (_REPO / ".gitignore").read_text()
|
||||
assert "/config/*.toml" in gi
|
||||
assert "!/config/instance.example.toml" in gi
|
||||
assert "/config/*" in gi
|
||||
assert "!/config/*.example.toml" in gi
|
||||
assert "!/config/.env.example" in gi
|
||||
|
||||
Reference in New Issue
Block a user