Skip to content

Commit 58ffe42

Browse files
committed
fix submissionStatus integ tests and has_changed attribute
1 parent e8e5361 commit 58ffe42

File tree

2 files changed

+135
-106
lines changed

2 files changed

+135
-106
lines changed

synapseclient/models/submission_status.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,22 @@ class SubmissionStatus(
421421
@property
422422
def has_changed(self) -> bool:
423423
"""Determines if the object has been newly created OR changed since last retrieval, and needs to be updated in Synapse."""
424+
if not self._last_persistent_instance:
425+
return True
426+
427+
model_attributes_changed = self._last_persistent_instance != self
428+
annotations_changed = (
429+
self._last_persistent_instance.annotations != self.annotations
430+
)
431+
submission_annotations_changed = (
432+
self._last_persistent_instance.submission_annotations
433+
!= self.submission_annotations
434+
)
435+
424436
return (
425-
not self._last_persistent_instance
426-
or self._last_persistent_instance is not self
437+
model_attributes_changed
438+
or annotations_changed
439+
or submission_annotations_changed
427440
)
428441

429442
def _set_last_persistent_instance(self) -> None:

tests/integration/synapseclient/models/synchronous/test_submission_status.py

Lines changed: 120 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import tempfile
5+
import test
56
import uuid
67
from typing import Callable
78

@@ -241,12 +242,9 @@ async def test_store_submission_status_with_submission_annotations(
241242

242243
# THEN the submission annotations should be saved
243244
assert updated_status.submission_annotations is not None
244-
converted_submission_annotations = from_submission_status_annotations(
245-
updated_status.submission_annotations
246-
)
247-
assert "score" in converted_submission_annotations
248-
assert converted_submission_annotations["score"] == [85.5]
249-
assert converted_submission_annotations["feedback"] == ["Good work!"]
245+
assert "score" in updated_status.submission_annotations
246+
assert updated_status.submission_annotations["score"] == [85.5]
247+
assert updated_status.submission_annotations["feedback"] == ["Good work!"]
250248

251249
async def test_store_submission_status_with_legacy_annotations(
252250
self, test_submission_status: SubmissionStatus
@@ -286,11 +284,8 @@ async def test_store_submission_status_with_combined_annotations(
286284

287285
# THEN both types of annotations should be saved
288286
assert updated_status.submission_annotations is not None
289-
converted_submission_annotations = from_submission_status_annotations(
290-
updated_status.submission_annotations
291-
)
292-
assert "public_score" in converted_submission_annotations
293-
assert converted_submission_annotations["public_score"] == [78.0]
287+
assert "public_score" in updated_status.submission_annotations
288+
assert updated_status.submission_annotations["public_score"] == [78.0]
294289

295290
assert updated_status.annotations is not None
296291
converted_annotations = from_submission_status_annotations(
@@ -310,16 +305,22 @@ async def test_store_submission_status_with_private_annotations_false(
310305
}
311306
test_submission_status.private_status_annotations = False
312307

313-
# AND I create the request body to inspect it
314-
request_body = test_submission_status.to_synapse_request(
315-
synapse_client=self.syn
308+
# WHEN I store the submission status
309+
updated_status = test_submission_status.store(synapse_client=self.syn)
310+
311+
# THEN they should be properly stored
312+
assert updated_status.annotations is not None
313+
converted_annotations = from_submission_status_annotations(
314+
updated_status.annotations
316315
)
316+
assert "public_internal_score" in converted_annotations
317+
assert converted_annotations["public_internal_score"] == 88.5
318+
assert converted_annotations["public_notes"] == "This should be visible"
317319

318-
# THEN the annotations should be marked as not private in the request
319-
assert "annotations" in request_body
320-
annotations_data = request_body["annotations"]
321-
assert "isPrivate" in annotations_data
322-
assert annotations_data["isPrivate"] is False
320+
# AND the annotations should be marked as not private
321+
for annos_type in ["stringAnnos", "doubleAnnos"]:
322+
annotations = updated_status.annotations[annos_type]
323+
assert all(not anno["isPrivate"] for anno in annotations)
323324

324325
async def test_store_submission_status_with_private_annotations_true(
325326
self, test_submission_status: SubmissionStatus
@@ -337,11 +338,23 @@ async def test_store_submission_status_with_private_annotations_true(
337338
synapse_client=self.syn
338339
)
339340

340-
# THEN the annotations should be marked as private in the request
341-
assert "annotations" in request_body
342-
annotations_data = request_body["annotations"]
343-
assert "isPrivate" in annotations_data
344-
assert annotations_data["isPrivate"] is True
341+
# WHEN I store the submission status
342+
updated_status = test_submission_status.store(synapse_client=self.syn)
343+
344+
# THEN they should be properly stored
345+
assert updated_status.annotations is not None
346+
converted_annotations = from_submission_status_annotations(
347+
updated_status.annotations
348+
)
349+
assert "private_internal_score" in converted_annotations
350+
assert converted_annotations["private_internal_score"] == 95.0
351+
assert converted_annotations["private_notes"] == "This should be private"
352+
353+
# AND the annotations should be marked as private
354+
for annos_type in ["stringAnnos", "doubleAnnos"]:
355+
annotations = updated_status.annotations[annos_type]
356+
print(annotations)
357+
assert all(anno["isPrivate"] for anno in annotations)
345358

346359
async def test_store_submission_status_without_id(self):
347360
"""Test that storing a submission status without ID raises ValueError."""
@@ -386,6 +399,57 @@ async def test_store_submission_status_change_tracking(
386399
# THEN has_changed should be False again
387400
assert not updated_status.has_changed
388401

402+
async def test_has_changed_property_edge_cases(
403+
self, test_submission_status: SubmissionStatus
404+
):
405+
"""Test the has_changed property with various edge cases and detailed scenarios."""
406+
# GIVEN a submission status that was just retrieved
407+
assert not test_submission_status.has_changed
408+
original_annotations = (
409+
test_submission_status.annotations.copy()
410+
if test_submission_status.annotations
411+
else {}
412+
)
413+
414+
# WHEN I modify only annotations (not submission_annotations)
415+
test_submission_status.annotations = {"test_key": "test_value"}
416+
417+
# THEN has_changed should be True
418+
assert test_submission_status.has_changed
419+
420+
# WHEN I reset annotations to the original value (should be the same as the persistent instance)
421+
test_submission_status.annotations = original_annotations
422+
423+
# THEN has_changed should be False (same as original)
424+
assert not test_submission_status.has_changed
425+
426+
# WHEN I add a different annotation value
427+
test_submission_status.annotations = {"different_key": "different_value"}
428+
429+
# THEN has_changed should be True
430+
assert test_submission_status.has_changed
431+
432+
# WHEN I store and get a fresh copy
433+
updated_status = test_submission_status.store(synapse_client=self.syn)
434+
fresh_status = SubmissionStatus(id=updated_status.id).get(
435+
synapse_client=self.syn
436+
)
437+
438+
# THEN the fresh copy should not have changes
439+
assert not fresh_status.has_changed
440+
441+
# WHEN I modify only submission_annotations
442+
fresh_status.submission_annotations = {"new_key": ["new_value"]}
443+
444+
# THEN has_changed should be True
445+
assert fresh_status.has_changed
446+
447+
# WHEN I modify a scalar field
448+
fresh_status.status = "VALIDATED"
449+
450+
# THEN has_changed should still be True
451+
assert fresh_status.has_changed
452+
389453

390454
class TestSubmissionStatusBulkOperations:
391455
"""Tests for bulk SubmissionStatus operations."""
@@ -576,75 +640,6 @@ async def test_batch_update_submission_statuses(
576640
assert "batch_score" in converted_submission_annotations
577641
assert converted_submission_annotations["batch_processed"] == ["true"]
578642

579-
async def test_batch_update_submission_statuses_large_batch(
580-
self, test_evaluation: Evaluation
581-
):
582-
"""Test batch update behavior with larger batch (approaching limits)."""
583-
# Note: This test demonstrates the pattern but doesn't create 500 submissions
584-
# as that would be too expensive for regular test runs
585-
586-
# GIVEN I have a list of statuses (simulated for this test)
587-
statuses = []
588-
589-
# WHEN I try to batch update (even with empty list)
590-
response = SubmissionStatus.batch_update_submission_statuses(
591-
evaluation_id=test_evaluation.id,
592-
statuses=statuses,
593-
synapse_client=self.syn,
594-
)
595-
596-
# THEN the operation should complete without error
597-
assert response is not None
598-
599-
async def test_batch_update_submission_statuses_with_batch_tokens(
600-
self, test_evaluation: Evaluation, test_submissions: list[Submission]
601-
):
602-
"""Test batch updating with batch tokens for multi-batch operations."""
603-
if len(test_submissions) < 2:
604-
pytest.skip("Need at least 2 submissions for batch token test")
605-
606-
# GIVEN multiple statuses split into batches
607-
all_statuses = []
608-
for submission in test_submissions:
609-
status = SubmissionStatus(id=submission.id).get(synapse_client=self.syn)
610-
status.status = "SCORED"
611-
all_statuses.append(status)
612-
613-
# Split into batches
614-
batch1 = all_statuses[:1]
615-
batch2 = all_statuses[1:]
616-
617-
# WHEN I update the first batch
618-
response1 = SubmissionStatus.batch_update_submission_statuses(
619-
evaluation_id=test_evaluation.id,
620-
statuses=batch1,
621-
is_first_batch=True,
622-
is_last_batch=len(batch2) == 0,
623-
synapse_client=self.syn,
624-
)
625-
626-
# THEN I should get a response (possibly with batch token)
627-
assert response1 is not None
628-
629-
# IF there's a second batch and we got a batch token
630-
if len(batch2) > 0:
631-
batch_token = (
632-
response1.get("batchToken") if isinstance(response1, dict) else None
633-
)
634-
635-
# WHEN I update the second batch with the token
636-
response2 = SubmissionStatus.batch_update_submission_statuses(
637-
evaluation_id=test_evaluation.id,
638-
statuses=batch2,
639-
is_first_batch=False,
640-
is_last_batch=True,
641-
batch_token=batch_token,
642-
synapse_client=self.syn,
643-
)
644-
645-
# THEN the second batch should also succeed
646-
assert response2 is not None
647-
648643

649644
class TestSubmissionStatusValidation:
650645
"""Tests for SubmissionStatus validation and error handling."""
@@ -717,15 +712,26 @@ async def test_fill_from_dict_with_complete_response(self):
717712
"canCancel": False,
718713
"cancelRequested": False,
719714
"annotations": {
720-
"stringAnnos": {"internal_note": ["This is internal"]},
721-
"doubleAnnos": {},
722-
"longAnnos": {},
723-
},
724-
"submissionAnnotations": {
725-
"stringAnnos": {"feedback": ["Great work!"]},
726-
"doubleAnnos": {"score": [92.5]},
727-
"longAnnos": {},
715+
"objectId": "123456",
716+
"scopeId": "9617645",
717+
"stringAnnos": [
718+
{
719+
"key": "internal_note",
720+
"isPrivate": True,
721+
"value": "This is internal",
722+
},
723+
{
724+
"key": "reviewer_notes",
725+
"isPrivate": True,
726+
"value": "Excellent work",
727+
},
728+
],
729+
"doubleAnnos": [
730+
{"key": "validation_score", "isPrivate": True, "value": 95.0}
731+
],
732+
"longAnnos": [],
728733
},
734+
"submissionAnnotations": {"feedback": ["Great work!"], "score": [92.5]},
729735
}
730736

731737
# WHEN I fill a SubmissionStatus from the response
@@ -742,11 +748,21 @@ async def test_fill_from_dict_with_complete_response(self):
742748
assert result.status_version == 2
743749
assert result.can_cancel is False
744750
assert result.cancel_requested is False
745-
assert "internal_note" in result.annotations
746-
assert result.annotations["internal_note"] == "This is internal"
751+
752+
# The annotations field should contain the raw submission status format
753+
assert result.annotations is not None
754+
assert "objectId" in result.annotations
755+
assert "scopeId" in result.annotations
756+
assert "stringAnnos" in result.annotations
757+
assert "doubleAnnos" in result.annotations
758+
assert len(result.annotations["stringAnnos"]) == 2
759+
assert len(result.annotations["doubleAnnos"]) == 1
760+
761+
# The submission_annotations should be in simple key-value format
747762
assert "feedback" in result.submission_annotations
748763
assert "score" in result.submission_annotations
749-
assert result.submission_annotations["score"] == 92.5
764+
assert result.submission_annotations["feedback"] == ["Great work!"]
765+
assert result.submission_annotations["score"] == [92.5]
750766

751767
async def test_fill_from_dict_with_minimal_response(self):
752768
"""Test filling a SubmissionStatus from a minimal API response."""

0 commit comments

Comments
 (0)