diff --git a/synapseclient/models/submission_status.py b/synapseclient/models/submission_status.py index 9db745552..280609e3c 100644 --- a/synapseclient/models/submission_status.py +++ b/synapseclient/models/submission_status.py @@ -1,3 +1,4 @@ +from copy import deepcopy from dataclasses import dataclass, field, replace from datetime import date, datetime from typing import Optional, Protocol, Union @@ -435,7 +436,14 @@ def has_changed(self) -> bool: def _set_last_persistent_instance(self) -> None: """Stash the last time this object interacted with Synapse. This is used to determine if the object has been changed and needs to be updated in Synapse.""" + del self._last_persistent_instance self._last_persistent_instance = replace(self) + self._last_persistent_instance.annotations = ( + deepcopy(self.annotations) if self.annotations else {} + ) + self._last_persistent_instance.submission_annotations = ( + deepcopy(self.submission_annotations) if self.submission_annotations else {} + ) def fill_from_dict( self, diff --git a/tests/unit/synapseclient/models/async/unit_test_submission_status_async.py b/tests/unit/synapseclient/models/async/unit_test_submission_status_async.py index ff4137c9e..7c122b1c9 100644 --- a/tests/unit/synapseclient/models/async/unit_test_submission_status_async.py +++ b/tests/unit/synapseclient/models/async/unit_test_submission_status_async.py @@ -478,6 +478,71 @@ async def test_batch_update_with_batch_token(self) -> None: assert request_body["batchToken"] == batch_token assert request_body["isFirstBatch"] is False + def test_in_place_mutation_of_submission_annotations_detected_by_has_changed( + self, + ) -> None: + """ + Test that in-place .update() on submission_annotations are + detected as a change by has_changed. + """ + # GIVEN a status with submission_annotations and a stashed persistent instance + submission_status = SubmissionStatus( + id=SUBMISSION_STATUS_ID, + submission_annotations={"initial_key": ["initial_value"]}, + ) + submission_status._set_last_persistent_instance() + assert not submission_status.has_changed + + # WHEN I mutate submission_annotations in-place via .update() + submission_status.submission_annotations.update({"added_key": ["added_value"]}) + + # THEN has_changed should detect the mutation + assert submission_status.has_changed + + def test_in_place_mutation_of_annotations_detected_by_has_changed(self) -> None: + """ + That that in-place .update() on annotations are detected as + a change by has_changed. + """ + # GIVEN a status with annotations and a stashed persistent instance + submission_status = SubmissionStatus( + id=SUBMISSION_STATUS_ID, + annotations={"initial_key": "initial_value"}, + ) + submission_status._set_last_persistent_instance() + assert not submission_status.has_changed + + # WHEN I mutate annotations in-place via .update() + submission_status.annotations.update({"added_key": "added_value"}) + + # THEN has_changed should detect the mutation + assert submission_status.has_changed + + async def test_store_async_sends_request_after_in_place_mutation(self) -> None: + """Regression test: store_async must call the API when submission_annotations + was mutated in-place via .update(), not skip it as 'no changes'.""" + # GIVEN a status with a stashed persistent instance + submission_status = SubmissionStatus( + id=SUBMISSION_STATUS_ID, + etag=ETAG, + status_version=STATUS_VERSION, + submission_annotations={"initial_key": ["initial_value"]}, + ) + submission_status._set_last_persistent_instance() + + # WHEN I mutate in-place and store + submission_status.submission_annotations.update({"added_key": ["added_value"]}) + + with patch( + "synapseclient.api.evaluation_services.update_submission_status", + new_callable=AsyncMock, + return_value=self.get_example_submission_status_dict(), + ) as mock_update: + await submission_status.store_async(synapse_client=self.syn) + + # THEN the API should have been called (not skipped) + mock_update.assert_called_once() + def test_set_last_persistent_instance(self) -> None: """Test setting the last persistent instance.""" # GIVEN a SubmissionStatus