diff --git a/src/routes/main_routes.py b/src/routes/main_routes.py index 3b669195..dcf46d9c 100644 --- a/src/routes/main_routes.py +++ b/src/routes/main_routes.py @@ -23,7 +23,7 @@ SkillDifficulty, validate_skill_progression, ) -from utils.code_review import CodeReviewManager +from utils.code_review import CodeReviewManager, SubmissionAlreadyExistsError from config import Config from utils.portfolio_analyzer import analyze_portfolio import math @@ -706,6 +706,8 @@ def submit_code_for_review(): "success": True, "submission": submission }), 201 + except SubmissionAlreadyExistsError as e: + return jsonify({"error": str(e)}), 409 except Exception as e: return jsonify({"error": str(e)}), 400 diff --git a/src/utils/code_review.py b/src/utils/code_review.py index ad4d3f7d..08fbdc59 100644 --- a/src/utils/code_review.py +++ b/src/utils/code_review.py @@ -10,6 +10,10 @@ from datetime import datetime, timezone +class SubmissionAlreadyExistsError(Exception): + """Raised when a code submission re-uses an existing submission_id.""" + + class ReviewStatus(Enum): """Status of a code review.""" PENDING = "pending" @@ -106,6 +110,11 @@ def submit_code( "metrics": {}, } + if submission_id in self.submissions: + raise SubmissionAlreadyExistsError( + f"Submission {submission_id} already exists" + ) + self.submissions[submission_id] = submission return submission diff --git a/tests/test_code_review.py b/tests/test_code_review.py index f8e6f720..ed891bf2 100644 --- a/tests/test_code_review.py +++ b/tests/test_code_review.py @@ -8,6 +8,7 @@ ReviewStatus, CodeQualityCategory, FEEDBACK_TEMPLATES, + SubmissionAlreadyExistsError, ) @@ -37,6 +38,66 @@ def test_submit_code(self, review_manager): assert submission["review_status"] == ReviewStatus.PENDING.value assert submission["submitted_at"] is not None + def test_submit_code_rejects_duplicate_id(self, review_manager): + """Re-submitting an existing submission_id must be rejected (issue #1871).""" + review_manager.submit_code( + submission_id="sub_001", + user_id="user_123", + project_id=1, + code="print('hello')", + language="python", + ) + + with pytest.raises(SubmissionAlreadyExistsError): + review_manager.submit_code( + submission_id="sub_001", + user_id="user_456", + project_id=2, + code="print('overwrite')", + language="python", + ) + + def test_resubmission_does_not_clobber_review_progress(self, review_manager): + """A rejected re-submit must leave review_count and metrics intact (issue #1871).""" + review_manager.submit_code( + submission_id="sub_001", + user_id="user_123", + project_id=1, + code="print('hello')", + language="python", + ) + review = review_manager.start_review( + submission_id="sub_001", + reviewer_id="reviewer_001", + ) + review_manager.score_category( + review_id=review["review_id"], + category="functionality", + score=90, + ) + review_manager.complete_review( + review_id=review["review_id"], + summary="Good work", + ) + + before = review_manager.get_submission("sub_001") + assert before["review_count"] == 1 + assert before["metrics"]["overall_score"] == 90.0 + + with pytest.raises(SubmissionAlreadyExistsError): + review_manager.submit_code( + submission_id="sub_001", + user_id="user_123", + project_id=1, + code="print('overwrite')", + language="python", + ) + + after = review_manager.get_submission("sub_001") + assert after["review_count"] == before["review_count"] + assert after["metrics"] == before["metrics"] + assert after["code"] == "print('hello')" + def test_get_submission(self, review_manager): """Test retrieving a submission.""" review_manager.submit_code(