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:
2026-06-09 23:45:08 +09:00
co-authored by Claude Opus 4.8
parent 6e8e79660c
commit 8b2c26c04c
14 changed files with 531 additions and 32 deletions
+7 -4
View File
@@ -4,8 +4,11 @@ __pycache__/
*.egg-info/ *.egg-info/
.pytest_cache/ .pytest_cache/
/.env /.env
**/.env
/.vscode/ /.vscode/
# BAT-013 / REQ-048: per-instance TOML configs hold instance_key + portfolio_hub_token # REQ-048/010 CHG-20260609-01 (trex-orb-v1 aligned): config/ holds real per-instance
# (secrets) — never commit. The tracked example below is exempt. # TOML configs + the shared .env (secrets) — never commit. Only example templates
/config/*.toml # (*.example.toml, .env.example) are tracked.
!/config/instance.example.toml /config/*
!/config/*.example.toml
!/config/.env.example
+57
View File
@@ -0,0 +1,57 @@
# trex-core shared infrastructure + secrets (12-factor, REQ-010 CHG-20260609-01).
#
# These are SHARED across all per-instance TOML configs in this directory (same
# DB / broker / portfolio-hub / RabbitMQ), so they live here instead of the
# per-instance TOML.
#
# Resolution (trex_core.runtime.config):
# config_dir = --config-path (default ./config)
# --env-file <path> wins, else <config_dir>/.env
# (standalone reads with no config fall back to legacy CWD-relative .env)
# Real exported environment variables ALWAYS take precedence over this file.
# Use --config-path <abs-dir> to stay CWD-independent when launching elsewhere.
#
# Copy this template to `.env` in the same config dir and fill in:
# cp config/.env.example config/.env
# --- Core (required) ---
# psycopg DSN for the PostgreSQL single-writer store.
TREX_POSTGRESQL_DSN=dbname=trex_core host=/var/run/postgresql
# --- Daemon (optional, defaults shown) ---
TREX_TOTAL_SLOTS=10
TREX_EXECUTION_INTERVAL_SEC=1.0
# --- Broker selection (REQ-011/012/013) ---
# 'paper' (deterministic test double, default) | 'kis' (real KIS Open API)
TREX_BROKER=paper
# --- Paper broker tuning (only when TREX_BROKER=paper; for e2e scenarios) ---
# TREX_PAPER_FILL_AFTER_POLLS=0
# TREX_PAPER_QTY_PER_POLL=
# TREX_PAPER_REJECT_SYMBOLS=
# --- KIS broker (only when TREX_BROKER=kis) ---
# 모의투자(sandbox): https://openapivts.koreainvestment.com:29443
# 실계좌(live): https://openapi.koreainvestment.com:9443 (requires TREX_KIS_ALLOW_LIVE=true)
TREX_KIS_BROKER_BASE_URL=https://openapivts.koreainvestment.com:29443
TREX_KIS_BROKER_TIMEOUT=5
TREX_KIS_BROKER_APP_KEY=
TREX_KIS_BROKER_APP_SECRET=
TREX_KIS_BROKER_ACCOUNT_REF=12345678-01
TREX_KIS_ALLOW_LIVE=false
TREX_KIS_REDIS_URL=
# --- portfolio-hub ledger (REQ-017/018/019, BAT-006) ---
# portfolio_hub_token is per-instance and lives in the instance TOML, not here.
TREX_PORTFOLIO_HUB_BASE_URL=
TREX_PORTFOLIO_HUB_TIMEOUT=5
TREX_LEDGER_INTERVAL_SEC=1.0
# --- Snapshot scheduler (REQ-030, BAT-008) ---
TREX_SNAPSHOT_SCHEDULE_KST=07:30
TREX_SNAPSHOT_TICK_SEC=60
# --- RabbitMQ transport (REQ-022, BAT-007) ---
TREX_RABBITMQ_URL=
TREX_RABBITMQ_TIMEOUT=5
@@ -0,0 +1,34 @@
# BAT-026: config 디렉터리 기반 설정 관리 — `--config-path`/`--env-file` + config-dir `.env` 공용화 (trex-orb-v1 정합)
- Status: release-candidate
- Profile: batch-lite
- Source Discovery: 없음 (trex-orb-v1 설정 관리 정합 — change-req 주도)
- Included REQ: REQ-048 (CHG-20260609-01), REQ-010 (CHG-20260609-01)
- Excluded REQ: 없음
## Summary
- trex-orb-v1의 config 디렉터리 기반 설정 관리(`--config <name>` + `--config-path <dir>` + config-dir `.env`)를 trex-core 에 이식한다.
- 핵심: `resolve_config_toml(name, config_path)`(이름→`<dir>/<name>.toml`, `.toml` 자동 부착, **슬래시 포함 값은 하위호환 명시 경로**)(REQ-048), `.env` 를 config-dir 기준 결정적 해석(`--env-file` 우선 → `<config_dir>/.env`)으로 변경 + `config/.env.example` + `.gitignore` `config/*` 허용목록(REQ-010). 데몬·운영자 CLI 양쪽에 `--config-path`/`--env-file` 적용.
- 안전: `from_env()` 의 legacy `.env` 로드(루트 `.env` 폴백)를 유지 → config/.env 부재 시에도 기존 루트 `.env` 로 동작(완전 하위호환). config/.env 존재 시 먼저 로드돼 우선(`override=False`).
## Scope
- In Scope:
- `resolve_config_toml` + `DEFAULT_CONFIG_DIR` (REQ-048)
- `_load_dotenv(config_path, env_file)` config-dir 결정적 해석 + legacy 폴백 (REQ-010)
- 데몬 `main` argparse `--config-path`/`--env-file` + `from_config(toml_path, env_file)`
- 운영자 CLI `--config-path`/`--env-file` + `_resolve_instance` 통일
- `.gitignore` `config/*` 허용목록, `config/.env.example` 신규
- Out of Scope:
- 실제 `.env` → `config/.env` 물리 이동(운영 배포 절차 — 루트 `.env` 폴백으로 무중단)
- TOML/env 레이어링 정책(키 우선순위) 변경 — 기존 유지
## Documents
- Planning: [planning.md](./planning.md)
- Design: none (batch-lite with no architecture impact)
- Implementation: [verification.md](./verification.md) 에 구현 요약 포함 (batch-lite)
- Verification: [verification.md](./verification.md)
## Notes
- Profile **batch-lite**: 국소 변경(config.py/daemon.py/cli.py + .gitignore + config 예제). 신규 런타임 흐름·계약 필드 없음(인자·해석 규칙 추가). `--config` 의미 확장은 인터페이스 영향이라 정식 change-req(REQ-048/010 CHG-20260609-01) 경유 → **Design Gate: no**.
- REQ-048/010 은 CHG-20260609-01 로 **Approved**(신규 AC 미구현). 본 batch release-candidate + confirm-req-implemented 로 **Implemented** 복귀.
- 발견 근거: 사용자 요청(trex-orb-v1 설정 관리 형태 정합).
@@ -0,0 +1,32 @@
# BAT-026 Planning
- Status: confirmed
- Batch: BAT-026
- Profile: batch-lite
## Included REQ
- REQ-048 (CHG-20260609-01) — `--config` 이름 해석 + `--config-path` + `resolve_config_toml`
- REQ-010 (CHG-20260609-01) — config-dir `.env` 결정적 해석 + `--env-file` + `config/.env.example` + `.gitignore`
## Delivery Plan
1. `config.py`: `DEFAULT_CONFIG_DIR` + `resolve_config_toml(name, config_path)`(슬래시 포함=명시 경로 하위호환). `_load_dotenv(config_path=None, env_file=None)` — `--env-file` 우선 → `<config_dir>/.env` → (config 컨텍스트 없으면) legacy `find_dotenv(usecwd=True)` 폴백. `from_config(toml_path, env_file=None)` 가 config-dir `.env` 선로드 후 `from_env()`.
2. `daemon.py main`: `--config-path`/`--env-file` argparse + `resolve_config_toml` + `from_config(path, env_file)`.
3. `cli.py`: `_add_common` 에 `--config-path`/`--env-file`; `_resolve_instance` 가 `resolve_config_toml` 경유; `main` 이 from_env 전 config-dir `.env` 선로드.
4. `.gitignore` `config/*` 허용목록(`*.example.toml`/`.env.example`), `config/.env.example` 신규.
5. 테스트: resolve_config_toml(이름/.toml/config-path/경로형), _load_dotenv(env-file/config-dir/override/부재/CWD무관), daemon·cli 인자.
## Dependencies
- Internal: `from_env()` 의 legacy `.env` 로드 유지가 하위호환의 핵심.
- External: 없음.
## Design Gate
- Design Required Before Implementation: no
- Reason: 국소 변경(3개 런타임 파일 + .gitignore + config 예제). 신규 계약 필드/런타임 흐름 없음. `--config` 의미 확장은 change-req(CHG-20260609-01)로 이미 정합.
## Milestones
1. config.py/daemon.py/cli.py + .gitignore + config/.env.example 구현.
2. 단위(resolve/dotenv/인자) green + 전체 회귀 + ruff.
## Risks
- R-1: config/.env 부재 시 키 미로드로 기동 실패. 대응: `from_env()` legacy 루트 `.env` 폴백 유지 → 무중단(완전 하위호환).
- R-2: `--config` 경로형 사용처 회귀. 대응: 슬래시 포함 값 명시 경로 폴백 + 회귀 테스트.
@@ -0,0 +1,45 @@
# BAT-026 Verification
- Status: approved
- Batch: BAT-026
- Profile: batch-lite
## Acceptance Mapping
### REQ-048 (CHG-20260609-01) — `--config` 이름 해석 + `--config-path`
| AC | Evidence |
|----|----------|
| AC-7: `--config <name> [--config-path <dir>]` → `<dir or ./config>/<name>.toml`(.toml 자동 부착) | `tests/runtime/test_config_resolution.py::test_resolve_name_default_dir` / `::test_resolve_appends_toml` / `::test_resolve_with_config_path` → PASS |
| AC-8: 슬래시 포함(경로형) 값은 명시 경로로 그대로 사용(하위호환) | `::test_resolve_path_form_passthrough` → PASS. 라이브: `trex-core universe --config config/test.toml` 정상(name 형태와 동일 결과) |
| AC-9: 운영자 CLI 도 `--config-path` 수용, `_resolve_instance` 동일 규칙 | `::test_cli_resolve_instance_honors_config_path` → PASS. 라이브: `--config test --config-path config` → instance=test 라우팅 |
### REQ-010 (CHG-20260609-01) — config-dir `.env` + `--env-file`
| AC | Evidence |
|----|----------|
| AC-1(갱신): `<config_dir>/.env`(또는 `--env-file`) 로드 → dsn 반영 | `::test_dotenv_loaded_from_config_dir_cwd_independent` → PASS |
| AC-2: OS env 우선(`override=False`) | `::test_dotenv_os_env_takes_precedence` + 기존 `test_config_dotenv.py::test_os_env_takes_precedence_over_dotenv` → PASS |
| AC-3: `.env` 부재 시 회귀 없음 | `::test_dotenv_absent_is_noop` + 기존 `test_no_dotenv_uses_os_env_only` → PASS |
| AC-4: `.gitignore` `config/*` 무시 + `*.example.toml`/`.env.example` 추적 | `test_instance_config.py::test_gitignore_excludes_instance_toml`(갱신) → PASS |
| AC-6: CWD 무관 결정적 로드 | `::test_dotenv_loaded_from_config_dir_cwd_independent`(다른 CWD 로 chdir 후 config-dir `.env` 로드) → PASS |
| AC-7: `--env-file` 우선 | `::test_dotenv_env_file_wins` → PASS |
## 하위호환 (R-1/R-2)
- `from_env()` 의 legacy `.env` 로드(루트 `.env` CWD 폴백) 유지 → config/.env 부재에도 기존 루트 `.env` 로 동작. 기존 `test_config_dotenv.py` 6건 전부 green(무회귀).
- 경로형 `--config` 무중단: 라이브 스모크에서 `--config config/test.toml` 와 `--config test --config-path config` 가 **byte 동일 스냅샷** 반환(instance=test).
## 검증 명령/결과
- 단위(config): `pytest tests/runtime/test_config_resolution.py test_config_dotenv.py test_instance_config.py -q` → **23 passed**.
- 전체 회귀: `pytest -q --deselect tests/adapters/broker/test_kis.py::test_sandbox_token_and_submit` → **375 passed, 35 skipped, 1 deselected**.
- lint: `ruff check`(config.py/daemon.py/cli.py + 신규/갱신 테스트) → All checks passed.
- 라이브 스모크: 실행 중 test 데몬에 `trex-core universe --config test --config-path config --json` 와 `--config config/test.toml --json` → 동일 응답(instance=test, 라우팅·.env 폴백 정상).
## Baseline 문서 동기화
- `config/.env.example` 신규(공유 인프라 키 + config-dir 해석 헤더). `docs/project-structure.md` 설정 계층 서술은 후속 정합(흐름 변화 없음 — 인자/해석 규칙 추가) — 구조 영향 없음(Design Gate: no).
## 결과 요약
- REQ-048 AC-7/8/9 + REQ-010 AC-1/2/3/4/6/7 전부 단위 PASS + 라이브 스모크 무회귀.
- **Ready for Release: true.**
## 미검증/한계
- 데몬 진입점(`daemon.py:main`)은 `# pragma: no cover`(프로세스 진입점) — `resolve_config_toml`/`from_config(env_file)` 단위 + CLI 라이브 스모크로 동등 경로 커버.
- 실제 `.env` → `config/.env` 물리 이동은 운영 배포 절차(루트 `.env` 폴백으로 무중단) — 본 batch 범위 밖.
+1
View File
@@ -33,3 +33,4 @@
| BAT-023 | standard | released | REQ-017 (CHG-20260608-02), REQ-019 (CHG-20260608-02), REQ-029 (CHG-20260608-01), REQ-030 (CHG-20260608-01, Approved→Implemented) | dcy-018 | [bat-023](./bat-023_20260608_ledger-cash-position-writeback-option-d/) | | BAT-023 | standard | released | REQ-017 (CHG-20260608-02), REQ-019 (CHG-20260608-02), REQ-029 (CHG-20260608-01), REQ-030 (CHG-20260608-01, Approved→Implemented) | dcy-018 | [bat-023](./bat-023_20260608_ledger-cash-position-writeback-option-d/) |
| BAT-024 | batch-lite | release-candidate | REQ-032 (CHG-20260609-01) | dcy-016 (issue #17) | [bat-024](./bat-024_20260609_operator-enter-forced-close-at/) | | BAT-024 | batch-lite | release-candidate | REQ-032 (CHG-20260609-01) | dcy-016 (issue #17) | [bat-024](./bat-024_20260609_operator-enter-forced-close-at/) |
| BAT-025 | standard | release-candidate | REQ-002 (CHG-20260609-01), REQ-003 (CHG-20260609-01), REQ-006 (CHG-20260609-01), REQ-007 (CHG-20260609-01), REQ-009 (CHG-20260609-01) | dcy-019 (issue #18) | [bat-025](./bat-025_20260609_slot-entry-count-and-closed-reallocation/) | | BAT-025 | standard | release-candidate | REQ-002 (CHG-20260609-01), REQ-003 (CHG-20260609-01), REQ-006 (CHG-20260609-01), REQ-007 (CHG-20260609-01), REQ-009 (CHG-20260609-01) | dcy-019 (issue #18) | [bat-025](./bat-025_20260609_slot-entry-count-and-closed-reallocation/) |
| BAT-026 | batch-lite | release-candidate | REQ-048 (CHG-20260609-01), REQ-010 (CHG-20260609-01) | 없음 (trex-orb-v1 정합) | [bat-026](./bat-026_20260609_config-dir-env-and-config-path/) |
@@ -10,18 +10,21 @@
## Requirement ## Requirement
- `DaemonConfig.from_env()` 가 `os.environ` 을 읽기 전에 `.env`(있으면)를 로드한다. python-dotenv 를 의존성에 추가하고, `.env` 는 `.gitignore` 에 등록해 커밋되지 않게 하며, `.env.example` 를 참고용으로 제공한다. `.env` 가 없거나 dotenv 미설치여도 기존 동작(os.environ 단독)을 유지한다(회귀 없음). - `DaemonConfig.from_env()` 가 `os.environ` 을 읽기 전에 `.env`(있으면)를 로드한다. python-dotenv 를 의존성에 추가하고, `.env` 는 `.gitignore` 에 등록해 커밋되지 않게 하며, `.env.example` 를 참고용으로 제공한다. `.env` 가 없거나 dotenv 미설치여도 기존 동작(os.environ 단독)을 유지한다(회귀 없음).
- (CHG-20260609-01, trex-orb-v1 정합) `.env` 해석은 **config 디렉터리 기준 결정적**이다 — `--env-file <path>` 우선, 없으면 `<config_dir>/.env`(TOML 옆). CWD 의존 상향 탐색(`find_dotenv(usecwd=True)`)을 사용하지 않는다. `.env` 는 `config/.env` 로 공용화하고 `config/.env.example` 를 제공하며, `.gitignore` 는 `config/*` 를 무시하되 `*.example.toml`/`.env.example` 만 추적한다. `override=False`(실제 OS env 우선)는 유지한다.
## Acceptance Criteria ## Acceptance Criteria
1. Given CWD 에 `.env`(`TREX_POSTGRESQL_DSN=...`)가 있고 해당 OS env 가 미설정, When `DaemonConfig.from_env()`, Then `.env` 값이 로드되어 `dsn` 에 반영된다. 1. (CHG-20260609-01 갱신) Given `<config_dir>/.env`(또는 `--env-file`)에 `TREX_POSTGRESQL_DSN=...` 가 있고 해당 OS env 가 미설정, When 설정 로드, Then `.env` 값이 로드되어 `dsn` 에 반영된다.
2. Given OS env 와 `.env` 에 같은 키가 다른 값으로 존재, When `from_env()`, Then OS env 값이 우선한다(.env 가 덮어쓰지 않음). 2. Given OS env 와 `.env` 에 같은 키가 다른 값으로 존재, When 로드, Then OS env 값이 우선한다(.env 가 덮어쓰지 않음, `override=False`).
3. Given `.env` 가 없음, When `from_env()`, Then 기존처럼 `os.environ` 만으로 동작한다(회귀 없음). 3. Given `.env` 가 없음, When 로드, Then 기존처럼 `os.environ` 만으로 동작한다(회귀 없음).
4. Given 저장소, When `.gitignore` 확인, Then `.env` 가 무시 대상이며 `.env.example` 은 추적된다. 4. Given 저장소, When `.gitignore` 확인, Then `config/*` 가 무시 대상이며 `config/*.example.toml`·`config/.env.example` 은 추적된다.
5. Given `broker=paper` 이고 `.env`/OS env 에 `TREX_PAPER_FILL_AFTER_POLLS`/`TREX_PAPER_QTY_PER_POLL`/`TREX_PAPER_REJECT_SYMBOLS` 가 설정, When `DaemonConfig.from_env()` 후 `_build_broker`, Then paper 더블이 해당 시나리오(지연/부분/거부, REQ-011 AC-2)로 구성된다. 미설정이면 `PaperBroker()` 기본(즉시 전량 체결·거부 없음)과 byte-for-byte 동일하다(회귀 없음). 5. Given `broker=paper` 이고 `.env`/OS env 에 `TREX_PAPER_FILL_AFTER_POLLS`/`TREX_PAPER_QTY_PER_POLL`/`TREX_PAPER_REJECT_SYMBOLS` 가 설정, When `DaemonConfig.from_env()` 후 `_build_broker`, Then paper 더블이 해당 시나리오(지연/부분/거부, REQ-011 AC-2)로 구성된다. 미설정이면 `PaperBroker()` 기본과 동일(회귀 없음).
6. (CHG-20260609-01) Given CWD 를 바꿔 실행, When 설정 로드, Then `<config_dir>/.env` 가 CWD 와 무관하게 결정적으로 로드된다(상향 탐색 없음).
7. (CHG-20260609-01) Given `--env-file <path>`, When 로드, Then `<config_dir>/.env` 대신 그 파일이 로드된다(우선).
## Impacted Area ## Impacted Area
- Docs: 없음(baseline 구조/인터페이스/런타임 흐름 변경 없음 — minor-change) - Docs: (CHG-20260609-01) `config/.env.example`(신규, 공유 인프라 키), `.env`→`config/.env` 이동
- Modules: pyproject.toml(python-dotenv), trex_core/runtime/config.py(load_dotenv, PaperBrokerConfig), trex_core/runtime/daemon.py(_build_broker paper 주입), .gitignore, .env.example(신규 + paper 튜닝 키) - Modules: pyproject.toml(python-dotenv), trex_core/runtime/config.py(load_dotenv→`_load_dotenv(config_path, env_file)` config-dir 해석, PaperBrokerConfig), trex_core/runtime/daemon.py(_build_broker paper 주입, `--env-file`), cli.py(`--env-file`), .gitignore(config/* 허용목록), .env.example(paper 튜닝 키)
- Tests: tests/runtime/test_config_dotenv.py (로드/우선순위/부재 회귀, paper 튜닝 기본/파싱) - Tests: tests/runtime/test_config_dotenv.py (로드/우선순위/부재 회귀, paper 튜닝 기본/파싱, config-dir·`--env-file`·CWD 무관)
## Notes ## Notes
- minor-change fast path: 단일 저위험 Configuration REQ, 구조/인터페이스/런타임 흐름 영향 없음(기동 시 env 소스 추가뿐). batch-lite 로 처리, design 불요. - minor-change fast path: 단일 저위험 Configuration REQ, 구조/인터페이스/런타임 흐름 영향 없음(기동 시 env 소스 추가뿐). batch-lite 로 처리, design 불요.
@@ -235,3 +238,81 @@
#### Validation Plan #### Validation Plan
- BAT-009 verification evidence 재참조로 충분. - BAT-009 verification evidence 재참조로 충분.
### CHG-20260609-01
- Date: 2026-06-09
- Author: Justin Ko
#### Change Summary
- (change-req, trex-orb-v1 설정 관리 정합) `.env` 해석을 **config 디렉터리 기준 결정적**으로 변경한다 — `--env-file <path>` 가 우선, 없으면 **`<config_dir>/.env`**(TOML 옆). 기존 `find_dotenv(usecwd=True)`(CWD 의존 상향 탐색)를 제거해 실행 위치와 무관하게 동일 `.env` 를 로드한다. `.env` 를 repo 루트 → **`config/.env`** 로 이동(공용화), `config/.env.example` 신설, `.gitignore` 를 `config/*` 무시 + `*.example.toml`/`.env.example` 만 추적하는 형태로 전환. `override=False`(실제 OS env 우선)는 유지. `config_dir` 는 REQ-048 CHG-20260609-01 의 `--config-path` 로 제공된다.
#### Intent
- 어디서 실행하든 동일한 **공유 인프라 `.env`**(DSN/RabbitMQ/KIS/portfolio-hub 등)를 결정적으로 로드한다. 다중 인스턴스가 같은 broker/DSN/hub 를 공유하도록 `.env` 를 인스턴스 TOML 과 분리해 config 디렉터리에 공용화(12-factor).
#### Acceptance Criteria Delta
- Added:
- AC-6: `.env` 는 `<config_dir>/.env`(TOML 옆)에서 **CWD 와 무관하게** 결정적으로 로드된다. dotenv 미설치/`.env` 부재 시 no-op(회귀 없음).
- AC-7: `--env-file <path>` 가 주어지면 `<config_dir>/.env` 대신 그 파일을 로드한다(우선).
- AC-8: `.gitignore` 는 `config/*` 를 무시하되 `config/*.example.toml` 과 `config/.env.example` 은 추적한다(실제 `.env`/인스턴스 TOML 비커밋).
- Updated:
- AC-1: "CWD 의 `.env`" → "`<config_dir>/.env`"(또는 `--env-file`) 로 로드 소스 변경.
- Removed: none
#### Impacted Area Delta
- Modules: `trex_core/runtime/config.py`(`_load_dotenv(config_path, env_file)` — config-dir 결정적 해석, `find_dotenv(usecwd=True)` 제거), `trex_core/runtime/daemon.py`·`cli.py`(`--env-file` 인자 전달)
- Docs: `config/.env.example`(신규, 공유 인프라 키), `.gitignore`(config/* 허용목록), `.env`(루트→`config/.env` 이동)
- Tests: `tests/runtime/test_config_dotenv.py`(config-dir 로드/`--env-file` 우선/CWD 무관/`override=False` 회귀/부재 no-op)
#### Delivery Trace Delta
- Batches: 신규 batch-lite(REQ-048 CHG-20260609-01 과 동일 배치)
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (신규 batch 에서 AC-6/7/8 + AC-1 변경 검증)
- Existing Implementation Invalidated: yes (`.env` 해석 위치가 CWD → config-dir 로 변경되어 기존 루트 `.env` 의존 동작이 바뀜 — 운영 배포 시 `.env` 이동 필요)
- Status Recommendation: Implemented → Approved (신규/변경 AC 미구현 — batch 재검증 후 Implemented 복귀)
- Follow-up Action: draft-batch(batch-lite) → run-batch-implementation → verification → confirm-req-implemented
#### Validation Plan
- 신규 BAT: `<config_dir>/.env` 결정적 로드(CWD 변경해도 동일), `--env-file` 우선, OS env 우선(`override=False`) 회귀, `.env` 부재 no-op 단위. `.gitignore` config/* 허용목록 가드.
### CHG-20260609-02
- Date: 2026-06-09
- Author: Justin Ko
#### Change Summary
- Status 변경: Approved → Implemented. BAT-026 release-candidate verification evidence 기반(CHG-20260609-01 AC-1/2/3/4/6/7).
#### Acceptance Criteria Delta
- Added/Updated/Removed: 없음
#### Impacted Area Delta
- Docs/Modules/Tests: 없음
#### Delivery Trace Delta
- Batches: BAT-026 (release-candidate)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no (BAT-026 에서 완료)
- Existing Implementation Invalidated: no
- Status Recommendation: Implemented
- Follow-up Action: 운영 배포 시 `.env`→`config/.env` 이동(루트 `.env` 폴백으로 무중단). release 단계 별도 진행.
#### Validation Plan
- tests/runtime/test_config_resolution.py(config-dir `.env`/`--env-file`/CWD 무관/override) + test_config_dotenv.py 6건 무회귀 + `.gitignore` 가드. 전체 375 passed, ruff clean.
@@ -14,6 +14,7 @@
- `config.py` 에 `DaemonInstanceConfig` dataclass(`instance_key: str`, `portfolio_hub_token: str`)와 `from_toml(path)` 로더를 추가한다(stdlib `tomllib` 사용). - `config.py` 에 `DaemonInstanceConfig` dataclass(`instance_key: str`, `portfolio_hub_token: str`)와 `from_toml(path)` 로더를 추가한다(stdlib `tomllib` 사용).
- `DaemonConfig` 구성 시 `--config <path>.toml` 의 TOML 값을 `from_env()` 결과와 병합하는 팩토리(예: `from_config(toml_path)`)를 추가한다. `portfolio_hub_token` 은 TOML 값을 사용해 `PortfolioHubConfig.token` 에 주입한다. - `DaemonConfig` 구성 시 `--config <path>.toml` 의 TOML 값을 `from_env()` 결과와 병합하는 팩토리(예: `from_config(toml_path)`)를 추가한다. `portfolio_hub_token` 은 TOML 값을 사용해 `PortfolioHubConfig.token` 에 주입한다.
- 데몬 기동 진입점은 `--config` 를 **필수 인자**로 받는다. 인자 없이 기동하면 오류 메시지를 출력하고 비정상 종료(exit code ≠ 0)한다. 기본값 fallback 없음(OQ-2 Answered). - 데몬 기동 진입점은 `--config` 를 **필수 인자**로 받는다. 인자 없이 기동하면 오류 메시지를 출력하고 비정상 종료(exit code ≠ 0)한다. 기본값 fallback 없음(OQ-2 Answered).
- (CHG-20260609-01, trex-orb-v1 정합) `--config` 는 **이름**으로 받고 `--config-path <dir>`(기본 `./config`)로 config 디렉터리를 분리한다. `resolve_config_toml(name, config_path)` 가 `<config_dir>/<name>.toml` 로 해석하며 `.toml` 을 자동 부착한다. **하위호환**: 슬래시 포함(경로형) `--config` 값은 명시 경로로 그대로 사용한다. 운영자 CLI(`trex-core`)도 `--config-path` 를 동일 규칙으로 수용한다.
- `instance_key` 와 `portfolio_hub_token` 은 `.env`/OS env 에서 읽지 않는다. TOML 미존재/필수 필드 누락 시 명확한 오류로 종료한다. - `instance_key` 와 `portfolio_hub_token` 은 `.env`/OS env 에서 읽지 않는다. TOML 미존재/필수 필드 누락 시 명확한 오류로 종료한다.
- `.gitignore` 에 인스턴스 TOML 경로 패턴(예: `*.toml` 또는 `config/*.toml`)을 추가해 설정 파일이 커밋되지 않게 한다. 토큰/instance_key 는 로그에 평문 출력하지 않는다(NFR-4). - `.gitignore` 에 인스턴스 TOML 경로 패턴(예: `*.toml` 또는 `config/*.toml`)을 추가해 설정 파일이 커밋되지 않게 한다. 토큰/instance_key 는 로그에 평문 출력하지 않는다(NFR-4).
@@ -25,14 +26,18 @@
4. Given TOML 에 필수 필드(`instance_key`) 누락, When `from_toml`/`from_config`, Then 명확한 오류로 실패한다(조용한 기본값 금지). 4. Given TOML 에 필수 필드(`instance_key`) 누락, When `from_toml`/`from_config`, Then 명확한 오류로 실패한다(조용한 기본값 금지).
5. Given 저장소, When `.gitignore` 확인, Then 인스턴스 TOML 경로 패턴이 무시 대상이다. 5. Given 저장소, When `.gitignore` 확인, Then 인스턴스 TOML 경로 패턴이 무시 대상이다.
6. Given 데몬 기동 로그, When 출력 확인, Then `portfolio_hub_token` 평문이 로그에 노출되지 않는다. 6. Given 데몬 기동 로그, When 출력 확인, Then `portfolio_hub_token` 평문이 로그에 노출되지 않는다.
7. (CHG-20260609-01) Given `--config <name> [--config-path <dir>]`, When `resolve_config_toml`, Then `<dir or ./config>/<name>.toml` 로 해석되고 `.toml` 이 없으면 자동 부착된다.
8. (CHG-20260609-01) Given 슬래시 포함(경로형) `--config` 값, When 해석, Then `--config-path` 와 무관하게 명시 경로로 그대로 사용된다(하위호환).
9. (CHG-20260609-01) Given 운영자 CLI 가 `--config-path` 와 함께 호출, When `_resolve_instance`, Then 동일 `resolve_config_toml` 규칙으로 TOML 을 찾아 instance_key 를 해석한다.
## Impacted Area ## Impacted Area
- Docs: - Docs:
- `docs/project-structure.md` — 설정 파일 계층(TOML + env) (REQ-051 처리) - `docs/project-structure.md` — 설정 파일 계층(TOML + env) (REQ-051 처리)
- Modules: - Modules:
- `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config` 병합 - `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config` 병합, (CHG-20260609-01) `resolve_config_toml`/`DEFAULT_CONFIG_DIR`
- 데몬 진입점(`trex_core/runtime/daemon.py:main` 또는 `__main__`) — `--config` 필수 파싱(argparse) - 데몬 진입점(`trex_core/runtime/daemon.py:main` 또는 `__main__`) — `--config` 필수 파싱(argparse), (CHG-20260609-01) `--config-path`
- 운영자 CLI `trex_core/runtime/cli.py` — (CHG-20260609-01) `--config-path` + `_resolve_instance` 통일
- `.gitignore` — 인스턴스 TOML 패턴 - `.gitignore` — 인스턴스 TOML 패턴
- Tests: - Tests:
- `tests/runtime/` — TOML 로더 파싱/필수 인자/토큰 주입/누락 오류 - `tests/runtime/` — TOML 로더 파싱/필수 인자/토큰 주입/누락 오류
@@ -186,3 +191,81 @@
#### Validation Plan #### Validation Plan
- BAT-018: from_toml 기본값(KST/00:00) + TOML 값 from_config 전파 단위. - BAT-018: from_toml 기본값(KST/00:00) + TOML 값 from_config 전파 단위.
### CHG-20260609-01
- Date: 2026-06-09
- Author: Justin Ko
#### Change Summary
- (change-req, trex-orb-v1 설정 관리 정합) `--config` 를 **경로 대신 이름**으로 받고 `--config-path <dir>`(기본 `./config`)로 config 디렉터리를 분리한다. `resolve_config_toml(name, config_path)` 추가 — `<config_dir>/<name>.toml`(`.toml` 자동 부착). **하위호환**: 슬래시 포함(경로형) `--config` 값은 명시 경로로 그대로 사용(기존 운영 기동 스크립트 무중단). 운영자 CLI(`trex-core`)에도 `--config-path` 를 적용해 인스턴스 라우팅을 동일 규칙으로 통일. 데몬·CLI 진입점 argparse 갱신.
#### Intent
- trex-orb-v1 과 동일한 config 디렉터리 기반 설정 관리로 통일 — CWD 독립·이름 기반 인스턴스 선택. 공유 `.env`(REQ-010 CHG-20260609-01)의 해석 기준 디렉터리(`config_dir`)를 `--config-path` 로 제공한다.
#### Acceptance Criteria Delta
- Added:
- AC-7: `--config <name> [--config-path <dir>]` → `<dir or ./config>/<name>.toml` 로 해석되고 `.toml` 확장자가 없으면 자동 부착된다(`resolve_config_toml`).
- AC-8: 슬래시를 포함한(경로형) `--config` 값은 `--config-path` 와 무관하게 **명시 경로**로 그대로 사용된다(하위호환 — 기존 `--config config/test.toml` 무중단).
- AC-9: 운영자 CLI(`trex-core`)도 `--config-path` 를 수용하며, `_resolve_instance` 가 동일 `resolve_config_toml` 규칙으로 TOML 을 찾는다.
- Updated:
- AC-2(필수 인자)는 유지 — `--config` 는 여전히 필수다(이름 또는 경로).
- Removed: none
#### Impacted Area Delta
- Modules: `trex_core/runtime/config.py`(`resolve_config_toml`, `DEFAULT_CONFIG_DIR`), `trex_core/runtime/daemon.py`(`main` argparse `--config-path`), `trex_core/runtime/cli.py`(`--config-path` + `_resolve_instance` 통일)
- Docs: `docs/project-structure.md`(설정 파일 계층 — config 디렉터리/이름 해석), `config/instance.example.toml`
- Tests: `tests/runtime/`(resolve_config_toml: 이름/.toml 부착/config-path/경로형 폴백, 데몬·CLI `--config-path` 라우팅)
#### Delivery Trace Delta
- Batches: 신규 batch-lite(REQ-010 CHG-20260609-01 과 동일 배치)
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (신규 batch 에서 AC-7/8/9 검증)
- Existing Implementation Invalidated: no (경로형 `--config` 는 하위호환 유지, 이름 해석은 additive) — 단 신규 AC 미구현
- Status Recommendation: Implemented → Approved (신규 AC-7/8/9 미구현 — batch 재검증 후 Implemented 복귀)
- Follow-up Action: draft-batch(batch-lite) → run-batch-implementation → verification → confirm-req-implemented
#### Validation Plan
- 신규 BAT: `resolve_config_toml("test")`→`config/test.toml`, `("test.toml")`→`config/test.toml`, `("prod","/etc/trex")`→`/etc/trex/prod.toml`, 경로형(`config/test.toml`)→그대로 단위. 데몬·CLI `--config-path` 기동/라우팅 단위. 기존 경로형 `--config` 회귀.
### CHG-20260609-02
- Date: 2026-06-09
- Author: Justin Ko
#### Change Summary
- Status 변경: Approved → Implemented. BAT-026 release-candidate verification evidence 기반(CHG-20260609-01 AC-7/8/9).
#### Acceptance Criteria Delta
- Added/Updated/Removed: 없음
#### Impacted Area Delta
- Docs/Modules/Tests: 없음
#### Delivery Trace Delta
- Batches: BAT-026 (release-candidate)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no (BAT-026 에서 완료)
- Existing Implementation Invalidated: no
- Status Recommendation: Implemented
- Follow-up Action: release 단계 별도 진행
#### Validation Plan
- tests/runtime/test_config_resolution.py(resolve_config_toml/경로형 폴백/CLI `--config-path` 라우팅) + 라이브 스모크(name·path 형태 동일 결과). 전체 375 passed, ruff clean.
+1 -1
View File
@@ -139,8 +139,8 @@
## Recent Change Log Summary ## Recent Change Log Summary
- 2026-06-09 :: REQ-048/010 change-req→Implemented (CHG-20260609-01/02, trex-orb-v1 설정 관리 정합, BAT-026): config 디렉터리 기반 설정 관리 도입 — `--config` 를 이름으로 해석 + `--config-path <dir>`(기본 `./config`) + `resolve_config_toml`(슬래시 포함 값은 하위호환 명시 경로)(REQ-048), `.env` 를 config-dir 결정적 해석(`--env-file` 우선, 없으면 `<config_dir>/.env`, CWD 무관)으로 변경 + `config/.env.example` + `.gitignore` `config/*` 허용목록(REQ-010). 운영자 CLI 도 `--config-path`/`--env-file` 동일 적용. `from_env()` legacy 루트 `.env` 폴백 유지로 완전 하위호환(`.env` 이동은 운영 배포 시 무중단). batch-lite(BAT-026) 단위 23 + 전체 375 passed, ruff clean, 라이브 스모크(name·path 동일 결과) → 둘 다 **Implemented**. (change-req/confirm-req-implemented)
- 2026-06-09 :: REQ-002/003/006/007/009 change-req (CHG-20260609-01, dcy-019/issue #18, BAT-025): slot-scoped `entry_count` 추가 + closed 슬롯 직접 재할당. daily_slot/`Slot` 에 `entry_count`(기본 0) 필드 + `EXPECTED_SLOT_FIELDS` + 멱등 마이그레이션 008(REQ-002/006), 상태 머신 `closed→pending_buy` 신규 전이(8→9 base, ALLOWED_TRANSITIONS 14→15)(REQ-002), `_apply_transition_fields` closed→pending_buy clean-slate + 모든 `→pending_buy` 시 entry_count +1(REQ-002), `find_allocatable_slot`(idle∪closed, `entry_count→updated_at→slot_id`) selector + in-memory 동등(REQ-006), `_admit_buy` find_idle_slot→find_allocatable_slot + from_state=slot.status(REQ-007), 테스트 계약 정합(REQ-003), baseline 2문서(data-model/runtime-flows) 정합(REQ-009). 기존 `daily_symbol_entries`(symbol-scoped) 무관·미변경. 전부 change-req(신규 REQ 없음, OQ-1/2/3). 단위 366 passed(+KIS sandbox 1건은 live .env URL 로 사전 환경 실패·무관), ruff clean. PG 게이트 마이그레이션/드리프트는 라이브 runtime DSN 미사용(파괴적 truncate 회피)·test DSN 자동 검증. 전부 **Implemented** 유지. (change-req) - 2026-06-09 :: REQ-002/003/006/007/009 change-req (CHG-20260609-01, dcy-019/issue #18, BAT-025): slot-scoped `entry_count` 추가 + closed 슬롯 직접 재할당. daily_slot/`Slot` 에 `entry_count`(기본 0) 필드 + `EXPECTED_SLOT_FIELDS` + 멱등 마이그레이션 008(REQ-002/006), 상태 머신 `closed→pending_buy` 신규 전이(8→9 base, ALLOWED_TRANSITIONS 14→15)(REQ-002), `_apply_transition_fields` closed→pending_buy clean-slate + 모든 `→pending_buy` 시 entry_count +1(REQ-002), `find_allocatable_slot`(idle∪closed, `entry_count→updated_at→slot_id`) selector + in-memory 동등(REQ-006), `_admit_buy` find_idle_slot→find_allocatable_slot + from_state=slot.status(REQ-007), 테스트 계약 정합(REQ-003), baseline 2문서(data-model/runtime-flows) 정합(REQ-009). 기존 `daily_symbol_entries`(symbol-scoped) 무관·미변경. 전부 change-req(신규 REQ 없음, OQ-1/2/3). 단위 366 passed(+KIS sandbox 1건은 live .env URL 로 사전 환경 실패·무관), ruff clean. PG 게이트 마이그레이션/드리프트는 라이브 runtime DSN 미사용(파괴적 truncate 회피)·test DSN 자동 검증. 전부 **Implemented** 유지. (change-req)
- 2026-06-09 :: REQ-032 change-req→Implemented (CHG-20260609-01/02, issue #17, BAT-024): `trex-core enter` 에 `--forced-close-at <ISO8601>` (절대시간) 옵션 추가 — 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각 지정. CLI 가 buy intent payload 의 기존 `forced_close_at` 필드에 매핑(계약 REQ-007/021·도메인 REQ-066·forced-close 루프 REQ-037 이미 구현·불변, CLI 노출 갭만 보완). 잘못된 형식 제출 전 거부, 미지정 시 미포함(하위호환). 상대시간(`--hold-minutes`) 미채택. AC-5/6 신규. batch-lite(BAT-024) 단위 검증 → **Implemented**. 단위 353 passed, ruff clean. (change-req/confirm-req-implemented) - 2026-06-09 :: REQ-032 change-req→Implemented (CHG-20260609-01/02, issue #17, BAT-024): `trex-core enter` 에 `--forced-close-at <ISO8601>` (절대시간) 옵션 추가 — 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각 지정. CLI 가 buy intent payload 의 기존 `forced_close_at` 필드에 매핑(계약 REQ-007/021·도메인 REQ-066·forced-close 루프 REQ-037 이미 구현·불변, CLI 노출 갭만 보완). 잘못된 형식 제출 전 거부, 미지정 시 미포함(하위호환). 상대시간(`--hold-minutes`) 미채택. AC-5/6 신규. batch-lite(BAT-024) 단위 검증 → **Implemented**. 단위 353 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-017/019/029 change-req + REQ-030 Approved→Implemented (dcy-018, BAT-023): ledger write 응답의 권위 cash/quantity 를 portfolio 스냅샷에 write-back(option D 활성화) — 매수/매도 직후 "수량 live·현금 stale" 비대칭 제거. `record_trade`→`LedgerWriteOutcome{result, *_after}`+어댑터 `cash_balance_after`/`asset_quantity_after` 파싱(new+duplicate)(REQ-017), `LedgerWriter` SnapshotStore write-back best-effort+`pending_cash_delta`(REQ-019), 추정 현금 오버레이(`cash_estimated` 라벨)+슬롯 오버레이 유지(REQ-029, OQ-1 B/OQ-4 i), option D 활성화로 REQ-030 Approved→Implemented. 전부 change-req(신규 REQ 없음, OQ-3 a). portfolio-hub#8(`*_after`) 동작 중. 단위 350 passed, 인프라 게이트 385 passed, ruff clean. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-017/019/029 change-req + REQ-030 Approved→Implemented (dcy-018, BAT-023): ledger write 응답의 권위 cash/quantity 를 portfolio 스냅샷에 write-back(option D 활성화) — 매수/매도 직후 "수량 live·현금 stale" 비대칭 제거. `record_trade`→`LedgerWriteOutcome{result, *_after}`+어댑터 `cash_balance_after`/`asset_quantity_after` 파싱(new+duplicate)(REQ-017), `LedgerWriter` SnapshotStore write-back best-effort+`pending_cash_delta`(REQ-019), 추정 현금 오버레이(`cash_estimated` 라벨)+슬롯 오버레이 유지(REQ-029, OQ-1 B/OQ-4 i), option D 활성화로 REQ-030 Approved→Implemented. 전부 change-req(신규 REQ 없음, OQ-3 a). portfolio-hub#8(`*_after`) 동작 중. 단위 350 passed, 인프라 게이트 385 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-028 + REQ-032 change-req→Implemented (CHG-20260608-01/02 / -03/04, dcy-016 후속, BAT-022): `trex-core slots` 가 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정(10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). snapshot envelope 에 read-only per-slot `slots[]`(canonical `slot_to_dict`: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/.../portfolio_id) additive 추가(REQ-028 AC-4, strategy `assets[]` 불변), `trex-core slots` 가 `slots[]`(실제 슬롯 상태) 표시·`--status` 슬롯 status 필터·`universe` 는 `assets[]` 유지(REQ-032). BAT-007 stub/BAT-008 envelope 슬롯 스키마 단일화. 신규 코드 데몬 + 실 인프라 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). 단위 338 passed, ruff clean. pytest 인프라 게이트는 Human Approval Memo 수용. 둘 다 **Implemented**. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-028 + REQ-032 change-req→Implemented (CHG-20260608-01/02 / -03/04, dcy-016 후속, BAT-022): `trex-core slots` 가 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정(10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). snapshot envelope 에 read-only per-slot `slots[]`(canonical `slot_to_dict`: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/.../portfolio_id) additive 추가(REQ-028 AC-4, strategy `assets[]` 불변), `trex-core slots` 가 `slots[]`(실제 슬롯 상태) 표시·`--status` 슬롯 status 필터·`universe` 는 `assets[]` 유지(REQ-032). BAT-007 stub/BAT-008 envelope 슬롯 스키마 단일화. 신규 코드 데몬 + 실 인프라 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). 단위 338 passed, ruff clean. pytest 인프라 게이트는 Human Approval Memo 수용. 둘 다 **Implemented**. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-034/021/037/038/039/032/042 change-req (dcy-017, BAT-021): 전략 이탈로 슬롯이 워치독(안전망) 소유로 넘어간 상태를 가시화(issue #12). snapshot RPC 응답에 holding 슬롯 워치독 상태(expired/last_seen) read-only 동봉(best-effort) + `WatchdogStore.last_seen`(REQ-034/021), `trex-core slots` WATCHDOG-OWNED 표시(REQ-032), safety-sell 워치독 소유 진입(WARN)/이탈(INFO)·발동(forced_close·stop_loss=WARN, take_profit·market_close=INFO) 로그+in-process 카운터(REQ-037/038/039), baseline 2문서 정합(REQ-042). best-effort(청산 무차단), Redis 미채택. 전부 change-req(신규 REQ 없음, OQ-3). 단위 335 passed, 인프라 게이트 369 passed, ruff clean. (change-req/confirm-req-implemented)
+87
View File
@@ -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"
+5 -3
View File
@@ -83,7 +83,9 @@ def test_config_required_cli_exits_nonzero():
def test_gitignore_excludes_instance_toml(): 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() gi = (_REPO / ".gitignore").read_text()
assert "/config/*.toml" in gi assert "/config/*" in gi
assert "!/config/instance.example.toml" in gi assert "!/config/*.example.toml" in gi
assert "!/config/.env.example" in gi
+22 -3
View File
@@ -30,7 +30,12 @@ from zoneinfo import ZoneInfo
from trex_core.adapters.messaging import topology from trex_core.adapters.messaging import topology
from trex_core.adapters.portfolio.pause_store import PostgreSQLPauseStore from trex_core.adapters.portfolio.pause_store import PostgreSQLPauseStore
from trex_core.runtime.config import DaemonConfig, DaemonInstanceConfig from trex_core.runtime.config import (
DaemonConfig,
DaemonInstanceConfig,
_load_dotenv,
resolve_config_toml,
)
_DEFAULT_TIMEOUT = 10.0 _DEFAULT_TIMEOUT = 10.0
@@ -42,7 +47,8 @@ def _resolve_instance(args) -> str:
if getattr(args, "instance", None): if getattr(args, "instance", None):
return args.instance return args.instance
if getattr(args, "config", None): if getattr(args, "config", None):
return DaemonInstanceConfig.from_toml(args.config).instance_key toml = resolve_config_toml(args.config, getattr(args, "config_path", None))
return DaemonInstanceConfig.from_toml(toml).instance_key
return topology.DEFAULT_INSTANCE_KEY return topology.DEFAULT_INSTANCE_KEY
@@ -248,8 +254,12 @@ def _fail(message: str, as_json: bool) -> None:
# arg parsing # arg parsing
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def _add_common(p) -> None: def _add_common(p) -> None:
p.add_argument("--config", help="per-instance TOML (canonical instance routing)") p.add_argument("--config", help="per-instance TOML name (resolved within --config-path) or path")
p.add_argument("--config-path", dest="config_path", default=None,
help="config directory (default ./config) for --config + .env")
p.add_argument("--instance", help="instance key (shortcut; overrides --config)") p.add_argument("--instance", help="instance key (shortcut; overrides --config)")
p.add_argument("--env-file", dest="env_file", default=None,
help="explicit .env path; overrides <config-dir>/.env")
p.add_argument("--json", action="store_true", help="machine-readable JSON output") p.add_argument("--json", action="store_true", help="machine-readable JSON output")
p.add_argument("--timeout", type=float, default=_DEFAULT_TIMEOUT) p.add_argument("--timeout", type=float, default=_DEFAULT_TIMEOUT)
@@ -313,6 +323,15 @@ def main(argv: list[str] | None = None, *, rpc=None, confirm=None) -> int:
correlation_id, timeout) -> dict`, `confirm(prompt) -> bool`. correlation_id, timeout) -> dict`, `confirm(prompt) -> bool`.
""" """
args = _build_parser().parse_args(argv) args = _build_parser().parse_args(argv)
# REQ-010 CHG-20260609-01: load the config-dir .env (or --env-file) before
# reading env. from_env() also runs the legacy root-.env fallback; the
# config-dir load wins (loaded first, override=False).
_toml = (
resolve_config_toml(args.config, getattr(args, "config_path", None))
if getattr(args, "config", None)
else None
)
_load_dotenv(_toml, getattr(args, "env_file", None))
cfg = DaemonConfig.from_env() cfg = DaemonConfig.from_env()
confirm = confirm or _default_confirm confirm = confirm or _default_confirm
+52 -7
View File
@@ -6,17 +6,57 @@ from dataclasses import dataclass, replace
from typing import Optional from typing import Optional
def _load_dotenv() -> None: DEFAULT_CONFIG_DIR = "config"
"""Best-effort load of a .env file into os.environ (REQ-010).
Real OS environment variables take precedence over .env (override=False),
matching 12-factor expectations. If python-dotenv is not installed or no def resolve_config_toml(name: str, config_path: Optional[str] = None) -> str:
.env exists, this is a no-op (no regression). """Resolve `--config <name>` within a config directory (REQ-048 CHG-20260609-01).
config_dir = `--config-path` if given, else `./config`. `<name>` selects the
TOML inside it; a missing `.toml` extension is appended.
Backward-compat: if `name` contains a path separator (e.g. `config/test.toml`),
it is treated as an explicit path and used as-is so existing ops scripts that
pass a path keep working.
resolve_config_toml("test") -> config/test.toml
resolve_config_toml("test.toml") -> config/test.toml
resolve_config_toml("prod", "/etc/trex") -> /etc/trex/prod.toml
resolve_config_toml("config/test.toml") -> config/test.toml (path-form)
"""
if os.sep in name or (os.altsep and os.altsep in name):
return name # explicit path (backward-compat)
config_dir = config_path or DEFAULT_CONFIG_DIR
filename = name if name.endswith(".toml") else f"{name}.toml"
return os.path.join(config_dir, filename)
def _load_dotenv(config_path: Optional[str] = None, env_file: Optional[str] = None) -> None:
"""Best-effort load of a .env file into os.environ (REQ-010 / CHG-20260609-01).
Resolution (trex-orb-v1 aligned, deterministic & CWD-independent given a
config_path): `env_file` (explicit) wins, else `<config_dir>/.env` next to the
TOML. With neither (standalone `from_env()`), fall back to legacy CWD-relative
`find_dotenv` so existing root `.env` setups keep working (backward-compat).
Real OS env vars always take precedence (`override=False`). python-dotenv
missing or the resolved file absent => no-op (no regression).
""" """
try: try:
from dotenv import find_dotenv, load_dotenv from dotenv import load_dotenv
except ImportError: # pragma: no cover - optional at runtime except ImportError: # pragma: no cover - optional at runtime
return return
if env_file:
if os.path.isfile(env_file):
load_dotenv(env_file, override=False)
return
if config_path:
path = os.path.join(os.path.dirname(os.path.abspath(config_path)), ".env")
if os.path.isfile(path):
load_dotenv(path, override=False)
return
# No config context: legacy CWD-relative discovery (backward-compat fallback).
from dotenv import find_dotenv
load_dotenv(find_dotenv(usecwd=True), override=False) load_dotenv(find_dotenv(usecwd=True), override=False)
@@ -252,13 +292,18 @@ class DaemonConfig:
) )
@staticmethod @staticmethod
def from_config(toml_path: str) -> "DaemonConfig": def from_config(toml_path: str, env_file: Optional[str] = None) -> "DaemonConfig":
"""Build config from env + a per-instance TOML file (REQ-048 / BAT-013). """Build config from env + a per-instance TOML file (REQ-048 / BAT-013).
`instance_key` and `portfolio_hub_token` come from TOML (not .env). `instance_key` and `portfolio_hub_token` come from TOML (not .env).
TOML `portfolio_hub_token` overrides the (env-derived) PortfolioHubConfig TOML `portfolio_hub_token` overrides the (env-derived) PortfolioHubConfig
token when a portfolio-hub base_url is configured. token when a portfolio-hub base_url is configured.
REQ-010 CHG-20260609-01: the config-dir `.env` (next to the TOML, or
`--env-file`) is loaded first so it wins over the legacy root `.env`
fallback loaded inside `from_env()` (both `override=False`).
""" """
_load_dotenv(toml_path, env_file)
base = DaemonConfig.from_env() base = DaemonConfig.from_env()
inst = DaemonInstanceConfig.from_toml(toml_path) inst = DaemonInstanceConfig.from_toml(toml_path)
pf = base.portfolio_hub pf = base.portfolio_hub
+14 -4
View File
@@ -66,7 +66,7 @@ from trex_core.application.preallocation_scheduler import DailyPreallocationSche
from trex_core.application.snapshot_scheduler import SnapshotScheduler from trex_core.application.snapshot_scheduler import SnapshotScheduler
from trex_core.application.watchdog import WatchdogStore from trex_core.application.watchdog import WatchdogStore
from trex_core.domain.slot import SlotService from trex_core.domain.slot import SlotService
from trex_core.runtime.config import DaemonConfig from trex_core.runtime.config import DaemonConfig, resolve_config_toml
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
@@ -375,11 +375,21 @@ def main() -> None: # pragma: no cover - process entrypoint
parser = argparse.ArgumentParser(prog="trex-core-daemon") parser = argparse.ArgumentParser(prog="trex-core-daemon")
parser.add_argument( parser.add_argument(
"--config", required=True, "--config", required=True,
help="per-instance TOML config path (instance_key, portfolio_hub_token). " help="per-instance TOML name (resolved within --config-path) or explicit "
"Required — no default (REQ-048 / dcy-009 OQ-2).", "path. Required — no default (REQ-048 / dcy-009 OQ-2).",
)
parser.add_argument(
"--config-path", default=None,
help="config directory (default ./config) for --config name resolution "
"and the matching .env (REQ-048/010 CHG-20260609-01).",
)
parser.add_argument(
"--env-file", default=None,
help="explicit .env path; overrides <config-dir>/.env (REQ-010 CHG-20260609-01).",
) )
args = parser.parse_args() args = parser.parse_args()
config = DaemonConfig.from_config(args.config) toml_path = resolve_config_toml(args.config, args.config_path)
config = DaemonConfig.from_config(toml_path, env_file=args.env_file)
Daemon(config).run() Daemon(config).run()