fix: preserve concurrent teacher decisions when model scoring fails

This commit is contained in:
2026-10-17 20:43:21 +08:00
parent f123078102
commit 050aa4e02a
2 changed files with 37 additions and 4 deletions
+29
View File
@@ -373,6 +373,35 @@ def test_model_declining_score_records_original_gap(sqlite_db):
assert score.evidence_gaps[0]["reason"] == "只有装置介绍"
def test_failed_model_call_cannot_overwrite_a_concurrent_teacher_review(sqlite_db, monkeypatch):
from wisegrade import scoring
db = sqlite_db
item = project(db)
created = services.create_batch(SimpleNamespace(course_version_id=1, rubric_version_id=1), db)
batch = db.get(ScoringBatch, created["id"])
criterion = batch.rubric_snapshot["criteria"][0]
batch.rubric_snapshot = {**batch.rubric_snapshot, "criteria": [criterion]}
batch.status, batch.progress_total, batch.progress_done = "running", 1, 0
score = CriterionScore(batch_id=batch.id, project_id=item.id, criterion_id=criterion["criterion_id"], status="pending")
db.add(score)
db.commit()
factory = sessionmaker(bind=db.get_bind(), expire_on_commit=False)
def teacher_then_failure(*args, **kwargs):
with factory() as teacher_db:
services.review_score(score.id, SimpleNamespace(score=0, comment="教师核定零分", reason="已核对"), teacher_db)
raise ModelError("模型响应超时")
monkeypatch.setattr(scoring, "_project_facts", lambda *args, **kwargs: {})
monkeypatch.setattr(scoring, "_score_criterion", teacher_then_failure)
failures, reviews = scoring._run_project(batch.id, item.id, factory, lambda *args: None, FLAT, lambda *a, **k: [])
db.expire_all()
saved = db.get(CriterionScore, score.id)
assert saved.status == "reviewed"
assert saved.teacher_score == 0 and saved.teacher_comment == "教师核定零分"
assert saved.error == ""
assert failures == reviews == 0
assert db.get(ScoringBatch, batch.id).progress_done == 1
def test_unrebuilt_course_document_does_not_complete_task(sqlite_db):
db = sqlite_db
doc = KnowledgeDocument(course_version_id=1, title="无页面资料", document_type="course",
+8 -4
View File
@@ -387,13 +387,17 @@ def _run_project(batch_id, project_id, session_factory, client_factory, embedder
except Exception as exc:
db.rollback()
score = db.scalar(select(CriterionScore).where(CriterionScore.batch_id == batch.id,
CriterionScore.project_id == project.id, CriterionScore.criterion_id == criterion["criterion_id"]))
CriterionScore.project_id == project.id, CriterionScore.criterion_id == criterion["criterion_id"])
.with_for_update().execution_options(populate_existing=True))
if not score:
score = CriterionScore(batch_id=batch.id, project_id=project.id, criterion_id=criterion["criterion_id"])
db.add(score)
score.status, score.error = STATUS_FAILED, str(exc)[:500]
db.commit()
failures += 1
# A teacher may have committed a decision while the request was in
# flight. The exception path must honour it just like success does.
if score.status != STATUS_REVIEWED and score.teacher_score is None:
score.status, score.error = STATUS_FAILED, str(exc)[:500]
db.commit()
failures += 1
db.execute(update(ScoringBatch).where(ScoringBatch.id == batch_id).values(progress_done=ScoringBatch.progress_done + 1))
db.commit()
return failures, reviews