From 31b2fa7cc60d5f1719b2f992b9432d3f57bc4ce2 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:02:20 +0800 Subject: [PATCH] fix: preserve legacy IBKR cycle results without release receipts Co-Authored-By: Codex --- application/execution_receipt_adapter.py | 10 +++ tests/test_execution_receipt_adapter.py | 83 ++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/application/execution_receipt_adapter.py b/application/execution_receipt_adapter.py index 16b7976..cf67732 100644 --- a/application/execution_receipt_adapter.py +++ b/application/execution_receipt_adapter.py @@ -32,6 +32,16 @@ def attach_cycle_execution_receipt( array; no status label or local marker is promoted to a fill. """ + runtime_loaded = report.get("runtime_release_receipt") + if ( + isinstance(runtime_loaded, Mapping) + and runtime_loaded.get("attestation_state") == "legacy_unattested" + and runtime_loaded.get("strategy_release") is None + ): + # Legacy targets remain evidence-missing; optional reporting must not + # turn an already-completed cycle into an HTTP failure and retry. + return report + summary = _combined_summary(execution_summary, reconciliation_record) status = str(summary.get("execution_status") or "").strip().lower() reconciliation_required = status == "pending_reconciliation" or _has_any(summary, _PENDING_KEYS) diff --git a/tests/test_execution_receipt_adapter.py b/tests/test_execution_receipt_adapter.py index 37877bf..461dc7b 100644 --- a/tests/test_execution_receipt_adapter.py +++ b/tests/test_execution_receipt_adapter.py @@ -1,7 +1,11 @@ from __future__ import annotations import unittest +from copy import deepcopy +import pytest + +from application.cycle_result import StrategyCycleResult from application.execution_receipt_adapter import attach_cycle_execution_receipt @@ -70,3 +74,82 @@ def test_expected_block_is_risk_blocked(self) -> None: ) self.assertEqual(report["execution_receipt"]["outcome"], "risk_blocked") + + +@pytest.mark.parametrize("revision", [None, "abc1234", "A" * 40]) +def test_attested_invalid_revision_still_rejects_receipt(revision): + report = _report() + report["runtime_release_receipt"]["strategy_release"]["strategy_revision"] = revision + + with pytest.raises(ValueError, match="strategy_revision must be"): + attach_cycle_execution_receipt(report, {}, {}, execution_failed=False) + + assert "execution_receipt" not in report + + +def test_missing_attestation_is_not_treated_as_legacy(): + report = _report() + del report["runtime_release_receipt"] + + with pytest.raises(ValueError, match="strategy_revision must be"): + attach_cycle_execution_receipt(report, {}, {}, execution_failed=False) + + assert "execution_receipt" not in report + + +@pytest.mark.parametrize("profile", ["soxl_soxx_trend_income", "tqqq_growth_income"]) +@pytest.mark.parametrize("execution_status", ["executed", "blocked"]) +def test_legacy_request_preserves_cycle_result_without_receipt( + strategy_module_factory, monkeypatch, profile, execution_status +): + module = strategy_module_factory(STRATEGY_PROFILE=profile, IBKR_DRY_RUN_ONLY="false") + observed = {"cycles": 0, "notifications": []} + submitted_count = 1 if execution_status == "executed" else 0 + result = "OK - executed" if submitted_count else "Blocked - no equity" + + def run_cycle(**_kwargs): + observed["cycles"] += 1 + return StrategyCycleResult( + result=result, + execution_summary={ + "execution_status": execution_status, + "orders_submitted": [{"symbol": "TEST"}] if submitted_count else [], + **({"no_op_reason": "no_equity"} if not submitted_count else {}), + }, + reconciliation_record_path="/tmp/offline-reconciliation.json", + ) + + def persist_report(report, **_kwargs): + observed["report"] = deepcopy(report) + return "/tmp/offline-runtime-report.json" + + monkeypatch.setattr(module, "is_market_open_now", lambda **_kwargs: True) + monkeypatch.setattr(module, "load_strategy_plugin_signals", lambda: ((), None)) + monkeypatch.setattr(module, "run_strategy_core", run_cycle) + monkeypatch.setattr(module, "persist_execution_report", persist_report) + monkeypatch.setattr( + module, + "_publish_runtime_failure_notification", + lambda **kwargs: observed["notifications"].append(type(kwargs["exc"]).__name__), + ) + + # Keep the real report builder, receipt adapter, and HTTP handler connected. + response = module.app.test_client().post("/run") + + assert response.status_code == 200 + assert response.get_data(as_text=True) == result + assert observed["cycles"] == 1 + assert observed["notifications"] == [] + report = observed["report"] + assert report["runtime_release_receipt"]["attestation_state"] == "legacy_unattested" + assert report["runtime_release_receipt"]["missing"] == ["strategy_release"] + assert "execution_receipt" not in report + assert report["summary"]["execution_status"] == execution_status + assert report["summary"]["orders_submitted_count"] == submitted_count + assert report["artifacts"]["reconciliation_record_path"] == "/tmp/offline-reconciliation.json" + assert report["status"] == ("ok" if submitted_count else "error") + if submitted_count: + assert report["errors"] == [] + else: + assert report["diagnostics"]["failure_category"] == "strategy_execution_blocked" + assert report["errors"][0]["stage"] == "strategy_execution"