Skip to content

Commit 1bf3ff6

Browse files
fix: preserve offline preflight across throttled scans (#63)
* fix: preserve offline preflight across throttled scans (app/production_outcome_runtime.py) * fix: preserve offline preflight across throttled scans (tests/test_production_outcome_runtime.py)
1 parent 69380c0 commit 1bf3ff6

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

app/production_outcome_runtime.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ async def run_scheduled_scan(self: Application) -> None:
6767
# After collection, examine collector state
6868
collector = getattr(self, "collector", None)
6969
last_errors = list(getattr(collector, "last_errors", []) or [])
70+
# A throttled/not-due scan can return before the collector populates
71+
# ``last_errors``. The live preflight still proved that every active X
72+
# source was unreachable, so preserve that provider-wide truth in the
73+
# outcome instead of mislabelling all sources complete.
74+
if os.environ.get("X_PROVIDER_PREFLIGHT", "").strip().casefold() == "offline":
75+
last_errors = [
76+
f"@{str(source.get('handle', '')).lstrip('@').strip().lower()}: "
77+
"provider_preflight_offline"
78+
for source in enabled
79+
if str(source.get("handle", "")).lstrip("@").strip()
80+
]
7081

7182
# Parse source-level outcomes from errors
7283
attempted_handles: set[str] = set()

tests/test_production_outcome_runtime.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33
from pathlib import Path
44
from types import SimpleNamespace
5+
from unittest.mock import patch
56

67
from app import production_outcome_runtime as runtime
78
from app.production_outcome import OutcomeStatus, load_outcome
@@ -21,10 +22,9 @@ def __init__(self):
2122
data={"last_auto_run": "2026-09-07T00:00:00+00:00"}
2223
)
2324
self.collector = SimpleNamespace(
24-
last_errors=[
25-
"@source_one: provider_preflight_offline",
26-
"@source_two: provider_preflight_offline",
27-
]
25+
# A not-due scan returns before the collector can populate
26+
# errors; live preflight must still govern the outcome.
27+
last_errors=[]
2828
)
2929

3030
async def run_scheduled_scan(self):
@@ -39,7 +39,8 @@ async def run(self):
3939
with tempfile.TemporaryDirectory() as tmp:
4040
runtime._OUTCOME_PATH = Path(tmp) / "outcome.json"
4141
runtime.start_run(run_id="concrete-app-test", trigger_event="test")
42-
await ConcreteApplication().run()
42+
with patch.dict("os.environ", {"X_PROVIDER_PREFLIGHT": "offline"}):
43+
await ConcreteApplication().run()
4344
outcome = load_outcome(runtime._OUTCOME_PATH)
4445
finally:
4546
runtime._OUTCOME_PATH = old_path

0 commit comments

Comments
 (0)