@@ -99,14 +99,16 @@ def download(*args):
9999 analyze .assert_not_called ()
100100
101101
102- def test_preference_change_during_local_conversion_stops_model_request (lead ):
102+ @pytest .mark .parametrize ("requeue" , [True , False ])
103+ def test_preference_change_during_local_conversion_stops_model_request (lead , requeue ):
103104 queue_document_extraction (lead )
104105 job = claim_next_extraction ()
105106
106107 def convert (* args ):
107108 lead .draft .ai_assistance_opted_out = True
108109 lead .draft .save ()
109- queue_document_extraction (lead )
110+ if requeue :
111+ queue_document_extraction (lead )
110112 return "local text" , 1
111113
112114 with (
@@ -120,9 +122,53 @@ def convert(*args):
120122 patch ("efile.services.document_extractions.get_default_model" , return_value = "test-model" ),
121123 patch ("efile.services.document_extractions.extract_fields_from_file" ) as extract ,
122124 ):
123- with pytest .raises (RuntimeError , match = "superseded" ):
124- process_document_extraction (job .pk , job .claim_token )
125+ assert process_document_extraction (job .pk , job .claim_token ) is None
125126 extract .assert_not_called ()
127+ assert record_extraction_failure (job .pk , job .claim_token , "Worker exited" ) is None
128+ job .refresh_from_db ()
129+ assert job .status == DocumentExtraction .Status .PENDING
130+ assert job .attempts == 0
131+ assert job .error == ""
132+ assert claim_next_extraction () is not None
133+
134+
135+ @pytest .mark .parametrize ("attempts_before" , [0 , 2 ])
136+ @pytest .mark .parametrize ("initial_opted_out" , [True , False ])
137+ @override_settings (DOCUMENT_EXTRACTION_MAX_ATTEMPTS = 3 )
138+ def test_preference_change_at_completion_refunds_only_obsolete_attempt (lead , attempts_before , initial_opted_out ):
139+ FilingDraft .objects .filter (pk = lead .draft_id ).update (ai_assistance_opted_out = initial_opted_out )
140+ job = queue_document_extraction (lead )
141+ DocumentExtraction .objects .filter (pk = job .pk ).update (attempts = attempts_before )
142+ claimed = claim_next_extraction ()
143+
144+ def analyze (* args , ** kwargs ):
145+ # Simulate an update that does not use the normal requeue endpoint.
146+ FilingDraft .objects .filter (pk = lead .draft_id ).update (ai_assistance_opted_out = not initial_opted_out )
147+ return {"document title" : "obsolete result" }
148+
149+ with (
150+ patch (
151+ "efile.services.document_extractions.S3UploadHandler" ,
152+ return_value = Mock (download_file = Mock (return_value = {"success" : True })),
153+ ),
154+ patch ("efile.services.document_extractions.limited_pdf" , fake_pdf ),
155+ patch ("efile.services.document_extractions.analyze_document" , side_effect = analyze ),
156+ ):
157+ assert process_document_extraction (job .pk , claimed .claim_token ) is None
158+ # The supervisor must treat the child's normal exit as a no-op.
159+ assert record_extraction_failure (job .pk , claimed .claim_token , "Worker exited" ) is None
160+ job .refresh_from_db ()
161+ assert job .status == DocumentExtraction .Status .PENDING
162+ assert job .attempts == attempts_before
163+ assert job .claim_token is None
164+ assert job .lease_expires_at is None
165+ assert job .error == ""
166+ lead .draft .refresh_from_db ()
167+ assert lead .draft .extracted_guesses == {}
168+ new_claim = claim_next_extraction ()
169+ assert new_claim is not None
170+ assert new_claim .attempts == attempts_before + 1
171+ assert new_claim .claim_token != claimed .claim_token
126172
127173
128174@override_settings (DOCUMENT_EXTRACTION_MAX_ATTEMPTS = 1 )
0 commit comments