22
33import os
44import tempfile
5+ import test
56import uuid
67from 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
390454class 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
649644class 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