diff --git a/scripts/reset_paper_state.py b/scripts/reset_paper_state.py index b6e745b..9f6765f 100644 --- a/scripts/reset_paper_state.py +++ b/scripts/reset_paper_state.py @@ -20,15 +20,19 @@ touched — that is the audit trail. Only the live breaker / position / order state is cleared; the ``orders`` table is operational, not the trail. -Usage: +Usage (SEMPRE como módulo, a partir da raiz do repo — `python scripts/reset_paper_state.py` +daria ``ModuleNotFoundError: src``, pois a raiz não estaria no ``sys.path``): python -m scripts.reset_paper_state # prompts for confirmation - python -m scripts.reset_paper_state --yes # skip the prompt + python -m scripts.reset_paper_state --yes # skip the prompt (non-interactive) python -m scripts.reset_paper_state --dry-run # report only, change nothing -Stop the orchestrator first (or restart it after) so it reloads the cleared -state instead of re-persisting the in-memory breaker: +Na VPS (dockerizado): pare o orchestrator para ele recarregar o estado limpo em vez +de re-persistir o breaker em memória; rode o reset DENTRO do container ``app`` (que +segue DE PÉ e compartilha o mesmo volume ``./data`` — ``exec`` exige um container +rodando, por isso mira ``app``, não o ``orchestrator`` parado). ``--yes`` é obrigatório +porque ``docker compose exec`` não tem TTY para o prompt (sem ele → EOFError): docker compose -f docker-compose.vps.yml stop orchestrator - python -m scripts.reset_paper_state --yes + docker compose -f docker-compose.vps.yml exec app python -m scripts.reset_paper_state --yes docker compose -f docker-compose.vps.yml start orchestrator """ from __future__ import annotations @@ -145,9 +149,16 @@ def db_provider() -> Any: return 0 if not args.yes: - reply = input( - "\nClear breaker + open-position book + orders? [y/N] " - ).strip().lower() + try: + reply = input( + "\nClear breaker + open-position book + orders? [y/N] " + ).strip().lower() + except EOFError: + # Sem TTY (ex.: `docker compose exec` sem -it) o prompt não pode ser + # respondido — aborta limpo apontando --yes, em vez de estourar um + # traceback de EOFError. + print("\nSem TTY para confirmar. Rode de novo com --yes para pular o prompt.") + return 1 if reply not in ("y", "yes"): print("Aborted.") return 1 diff --git a/tests/test_reset_paper_state.py b/tests/test_reset_paper_state.py index 2b233c6..866bdad 100644 --- a/tests/test_reset_paper_state.py +++ b/tests/test_reset_paper_state.py @@ -8,6 +8,8 @@ """ from __future__ import annotations +from scripts.reset_paper_state import main as reset_main +from scripts.reset_paper_state import reset_paper_state from src.core.ledger import TradingLedger from src.hitl.orders import Order, OrderStore from src.orchestration.position_store import ( @@ -16,7 +18,6 @@ load_circuit_state, save_circuit_state, ) -from scripts.reset_paper_state import reset_paper_state _POS = { "symbol": "BTC/USDT", "side": "buy", "entry_price": 100.0, "quantity": 0.5, @@ -161,3 +162,44 @@ def db(): assert result["dry_run"] is True assert result["orders_cleared"] == 1 # what WOULD be removed assert store.count() == 1 # ...but untouched + + +# --------------------------------------------------------------------- main() CLI +# M4: o ledger db E o app db derivam de LEDGER_DIR (get_db_path), então apontá-lo ao +# tmp isola os dois; main() lê esse mesmo estado. +def _seed_dirty(monkeypatch, tmp_path): + monkeypatch.setenv("LEDGER_DIR", str(tmp_path)) + ledger = TradingLedger() + + def db(): + return ledger.db_path + + PositionStore(db).upsert("o1", _POS) + save_circuit_state(db, 123.0, 6, -74.5) + return db + + +def test_main_yes_resets_without_prompting(tmp_path, monkeypatch): + db = _seed_dirty(monkeypatch, tmp_path) + + def _no_input(*_a, **_k): + raise AssertionError("input() não deve ser chamado com --yes") + + monkeypatch.setattr("builtins.input", _no_input) + assert reset_main(["--yes"]) == 0 + assert PositionStore(db).count() == 0 + assert load_circuit_state(db) is None + + +def test_main_eof_without_yes_aborts_cleanly(tmp_path, monkeypatch): + # M4: sem --yes e sem TTY (docker exec sem -it), input() levanta EOFError — o + # script deve abortar limpo (return 1, sem traceback) e NÃO tocar o estado. + db = _seed_dirty(monkeypatch, tmp_path) + + def _raise_eof(*_a, **_k): + raise EOFError + + monkeypatch.setattr("builtins.input", _raise_eof) + assert reset_main([]) == 1 + assert PositionStore(db).count() == 1 + assert load_circuit_state(db) is not None