From 7f17c9b9f29ad91bdaea998d809502c959b72b9e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 30 Mar 2026 13:16:27 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20address=20PR=20#177=20review=20=E2=80=94?= =?UTF-8?q?=20move=20import=20os=20to=20top-level,=20add=20audit=20idempot?= =?UTF-8?q?ency=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move `import os` from inside evaluate_game() to module top-level imports (lazy imports are only for circular dependency avoidance) - Add get_or_none idempotency guard before RefractorBoostAudit.create() inside db.atomic() to prevent IntegrityError on UNIQUE(card_state, tier) constraint in PostgreSQL when apply_tier_boost is called twice for the same tier - Update atomicity test stub to provide card_state/tier attributes for the new Peewee expression in the idempotency guard Co-Authored-By: Claude Opus 4.6 (1M context) --- app/routers_v2/refractor.py | 4 ++-- app/services/refractor_boost.py | 6 +++++- tests/test_refractor_boost_integration.py | 14 +++++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/routers_v2/refractor.py b/app/routers_v2/refractor.py index daaee0e..0b1c78d 100644 --- a/app/routers_v2/refractor.py +++ b/app/routers_v2/refractor.py @@ -1,3 +1,5 @@ +import os + from fastapi import APIRouter, Depends, HTTPException, Query import logging from typing import Optional @@ -307,8 +309,6 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") - import os - from ..db_engine import RefractorCardState, Player, StratPlay from ..services.refractor_boost import apply_tier_boost from ..services.refractor_evaluator import evaluate_card diff --git a/app/services/refractor_boost.py b/app/services/refractor_boost.py index 700a820..3eb1bd1 100644 --- a/app/services/refractor_boost.py +++ b/app/services/refractor_boost.py @@ -667,7 +667,11 @@ def apply_tier_boost( audit_data["battingcard"] = new_card.id else: audit_data["pitchingcard"] = new_card.id - _audit_model.create(**audit_data) + existing_audit = _audit_model.get_or_none( + (_audit_model.card_state == card_state.id) & (_audit_model.tier == new_tier) + ) + if existing_audit is None: + _audit_model.create(**audit_data) # 8b. Update RefractorCardState — this is the SOLE tier write on tier-up. card_state.current_tier = new_tier diff --git a/tests/test_refractor_boost_integration.py b/tests/test_refractor_boost_integration.py index 636f3ba..7635245 100644 --- a/tests/test_refractor_boost_integration.py +++ b/tests/test_refractor_boost_integration.py @@ -1115,7 +1115,19 @@ class TestAtomicity: """ class _FailingAuditModel: - """Stub that raises on .create() to simulate audit write failure.""" + """Stub that raises on .create() to simulate audit write failure. + + Provides card_state/tier attributes for the Peewee expression in the + idempotency guard, and get_or_none returns None so it proceeds to + create(), which then raises to simulate the failure. + """ + + card_state = RefractorBoostAudit.card_state + tier = RefractorBoostAudit.tier + + @staticmethod + def get_or_none(*args, **kwargs): + return None @staticmethod def create(**kwargs):