Skip to content

Commit 8a60650

Browse files
Pigbibicodex
andcommitted
fix: fail closed on unreadable automation ledger
Co-Authored-By: Codex <noreply@openai.com>
1 parent e381ee9 commit 8a60650

5 files changed

Lines changed: 50 additions & 7 deletions

service/automation_decision.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,12 @@ def decide_automation_execution(
442442
"failure_history_complete": failure_history_complete,
443443
"human_review_required": human_review_required,
444444
"auto_fix_allowed": action == EXECUTION_RUN and effective_mode == MODE_REVIEW_AND_FIX and not human_review_required,
445-
"auto_merge_allowed": action == EXECUTION_RUN and effective_autonomy == AUTONOMY_AUTO_MERGE and not human_review_required,
445+
"auto_merge_allowed": (
446+
action == EXECUTION_RUN
447+
and effective_mode == MODE_REVIEW_AND_FIX
448+
and effective_autonomy == AUTONOMY_AUTO_MERGE
449+
and not human_review_required
450+
),
446451
"defer": defer,
447452
"reasons": reasons or ["execution allowed"],
448453
}

service/automation_run_ledger.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ def __init__(
225225
self._load_from_disk()
226226

227227
def _load_from_disk(self) -> None:
228-
if self._storage_path is None or not self._storage_path.exists():
228+
if self._storage_path is None:
229+
return
230+
if not self._storage_path.exists():
231+
self._history_completeness_unknown = True
229232
return
230233
runs, sequence, evicted_runs, evicted_runs_by_repo, history_unknown = self._read_from_disk_unlocked()
231234
with self._lock:
@@ -238,14 +241,14 @@ def _load_from_disk(self) -> None:
238241

239242
def _read_from_disk_unlocked(self) -> tuple[dict[str, dict[str, Any]], int, int, dict[str, int], bool]:
240243
if self._storage_path is None or not self._storage_path.exists():
241-
return {}, 0, 0, {}, False
244+
return {}, 0, 0, {}, self._storage_path is not None
242245
try:
243246
payload = json.loads(self._storage_path.read_text(encoding="utf-8"))
244247
except (OSError, json.JSONDecodeError):
245-
return {}, 0, 0, {}, False
248+
return {}, 0, 0, {}, True
246249
runs = payload.get("runs") if isinstance(payload, dict) else None
247250
if not isinstance(runs, dict):
248-
return {}, 0, 0, {}, False
251+
return {}, 0, 0, {}, True
249252
clean_runs = {str(key): value for key, value in runs.items() if isinstance(value, dict)}
250253
sequence = _safe_int(payload.get("sequence"), len(clean_runs))
251254
history_unknown = "evicted_runs" not in payload or "evicted_runs_by_repo" not in payload
@@ -278,7 +281,10 @@ def _persist_locked(self) -> None:
278281
self._persist_with_owner_guard_locked()
279282

280283
def _refresh_from_disk_locked(self) -> None:
281-
if self._storage_path is None or not self._storage_path.exists():
284+
if self._storage_path is None:
285+
return
286+
if not self._storage_path.exists():
287+
self._history_completeness_unknown = True
282288
return
283289
lock_handle = None
284290
try:

tests/test_ai_gateway_service_get_routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ def test_automation_triage_reports_retryable_incident(self) -> None:
672672
self.assertTrue(triage["retry_allowed"])
673673
self.assertEqual(triage["recommended_action"], "retry")
674674
self.assertEqual(triage["file_risk"], "low")
675-
self.assertEqual(triage["control"]["execution"]["action"], "review_only")
675+
self.assertEqual(triage["control"]["execution"]["action"], "human_review")
676676
self.assertIn("run_id=incident-123", triage["summary"])
677677
finally:
678678
server.shutdown()

tests/test_automation_decision.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ def test_auto_merge_requires_matching_repo_autonomy(self) -> None:
8282
self.assertEqual(result["effective_autonomy"], "auto_merge")
8383
self.assertTrue(result["auto_merge_allowed"])
8484

85+
def test_auto_merge_is_disabled_when_effective_mode_is_review_only(self) -> None:
86+
result = decide_automation_execution(
87+
repo="QuantStrategyLab/AIAuditBridge",
88+
requested_mode="auto_merge",
89+
control_action=CONTROL_CONTINUE,
90+
service_health="degraded",
91+
quota_status="ok",
92+
org_health_status="ok",
93+
policy={"default": {"max_autonomy": "auto_merge"}},
94+
)
95+
96+
self.assertEqual(result["action"], EXECUTION_RUN)
97+
self.assertEqual(result["effective_mode"], MODE_REVIEW_ONLY)
98+
self.assertFalse(result["auto_fix_allowed"])
99+
self.assertFalse(result["auto_merge_allowed"])
100+
85101
def test_degraded_health_forces_review_only(self) -> None:
86102
result = decide_automation_execution(
87103
repo="QuantStrategyLab/AIAuditBridge",

tests/test_automation_run_ledger.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,22 @@ def test_pre_migration_ledger_marks_history_completeness_unknown(self) -> None:
248248

249249
self.assertTrue(snapshot["summary"]["retention"]["history_completeness_unknown"])
250250

251+
def test_missing_persisted_ledger_marks_history_completeness_unknown(self) -> None:
252+
with TemporaryDirectory() as tmp:
253+
ledger = AutomationRunLedger(max_runs=2, storage_path=Path(tmp) / "missing.json")
254+
snapshot = ledger.snapshot(limit=None)
255+
256+
self.assertTrue(snapshot["summary"]["retention"]["history_completeness_unknown"])
257+
258+
def test_corrupt_persisted_ledger_marks_history_completeness_unknown(self) -> None:
259+
with TemporaryDirectory() as tmp:
260+
path = Path(tmp) / "automation_runs.json"
261+
path.write_text("{not-json", encoding="utf-8")
262+
ledger = AutomationRunLedger(max_runs=2, storage_path=path)
263+
snapshot = ledger.snapshot(limit=None)
264+
265+
self.assertTrue(snapshot["summary"]["retention"]["history_completeness_unknown"])
266+
251267
def test_update_preserves_control_fields_when_omitted(self) -> None:
252268
self.ledger.record(
253269
"run-1",

0 commit comments

Comments
 (0)