@@ -116,21 +116,78 @@ class ProviderWithIncludeFilter(GenericProvider):
116116 "provider_class" ,
117117 [GenericProvider , AquaComms ],
118118)
119- def test_provider_gets_mlparser (provider_class ):
120- """Test to check the any provider gets a default ML parser when ENV is activated ."""
119+ def test_provider_falls_back_to_openai (provider_class ):
120+ """Test that OpenAI parser is used as last resort when all other processors fail ."""
121121 os .environ ["PARSER_OPENAI_API_KEY" ] = "some_api_key"
122122 data = NotificationData .init_from_raw ("text/plain" , b"fake data" )
123123 data .add_data_part ("text/html" , b"other data" )
124124
125125 provider = provider_class ()
126+ original_processor_count = len (provider ._processors ) # pylint: disable=protected-access
127+
128+ with patch ("circuit_maintenance_parser.processor.GenericProcessor.process" ) as mock_processor :
129+ # All native processors fail, then OpenAI processor succeeds
130+ mock_processor .side_effect = [ProcessorError ] * original_processor_count + [[{"a" : "b" }]]
131+ provider .get_maintenances (data )
132+ # Native processors + 1 OpenAI call
133+ assert mock_processor .call_count == original_processor_count + 1
134+
135+ # OpenAI processor should NOT be appended to the provider's processor list
136+ assert len (provider ._processors ) == original_processor_count # pylint: disable=protected-access
137+ os .environ .pop ("PARSER_OPENAI_API_KEY" , None )
138+
139+
140+ def test_provider_does_not_use_openai_when_native_succeeds ():
141+ """Test that OpenAI parser is not invoked when a native processor succeeds."""
142+ os .environ ["PARSER_OPENAI_API_KEY" ] = "some_api_key"
143+ data = NotificationData .init_from_raw ("text/plain" , b"fake data" )
144+
145+ provider = GenericProvider ()
126146
127147 with patch ("circuit_maintenance_parser.processor.GenericProcessor.process" ) as mock_processor :
128148 mock_processor .return_value = [{"a" : "b" }]
129149 provider .get_maintenances (data )
150+ # Only the native processor should be called
151+ assert mock_processor .call_count == 1
130152
131- assert provider ._processors [- 1 ] == CombinedProcessor ( # pylint: disable=protected-access
132- data_parsers = [EmailDateParser , OpenAIParser ]
133- )
153+ os .environ .pop ("PARSER_OPENAI_API_KEY" , None )
154+
155+
156+ def test_provider_data_not_mutated_when_native_succeeds ():
157+ """Test that add_subject_to_text is not called when native processors succeed."""
158+ os .environ ["PARSER_OPENAI_API_KEY" ] = "some_api_key"
159+ data = NotificationData .init_from_raw ("text/plain" , b"fake data" )
160+ data .add_data_part ("email-header-subject" , b"Test Subject" )
161+
162+ original_content = data .data_parts [0 ].content
163+ provider = GenericProvider ()
164+
165+ with patch ("circuit_maintenance_parser.processor.GenericProcessor.process" ) as mock_processor :
166+ mock_processor .return_value = [{"a" : "b" }]
167+ provider .get_maintenances (data )
168+
169+ # Data should not have been mutated since native processor succeeded
170+ assert data .data_parts [0 ].content == original_content
171+ os .environ .pop ("PARSER_OPENAI_API_KEY" , None )
172+
173+
174+ def test_provider_no_repeated_append_on_multiple_calls ():
175+ """Test that calling get_maintenances multiple times doesn't grow the processor list."""
176+ os .environ ["PARSER_OPENAI_API_KEY" ] = "some_api_key"
177+ data = NotificationData .init_from_raw ("text/plain" , b"fake data" )
178+
179+ provider = GenericProvider ()
180+ original_processor_count = len (provider ._processors ) # pylint: disable=protected-access
181+
182+ with patch ("circuit_maintenance_parser.processor.GenericProcessor.process" ) as mock_processor :
183+ mock_processor .return_value = [{"a" : "b" }]
184+ provider .get_maintenances (data )
185+ provider .get_maintenances (data )
186+ provider .get_maintenances (data )
187+
188+ # Processor list should never grow
189+ assert len (provider ._processors ) == original_processor_count # pylint: disable=protected-access
190+ os .environ .pop ("PARSER_OPENAI_API_KEY" , None )
134191
135192
136193def test_add_subject_to_text_appends_subject_to_text_parts ():
@@ -230,3 +287,27 @@ def test_add_subject_to_text_handles_decode_errors():
230287
231288 # This should not raise an exception due to errors="ignore" in decode()
232289 provider .add_subject_to_text (data )
290+
291+
292+ def test_add_subject_to_text_skips_text_calendar ():
293+ """Test that add_subject_to_text does not modify text/calendar parts."""
294+ provider = GenericProvider ()
295+
296+ data = NotificationData ()
297+ data .add_data_part ("email-header-subject" , b"Planned Work Notification" )
298+ data .add_data_part (
299+ "text/calendar" ,
300+ b"BEGIN:VCALENDAR\r \n VERSION:2.0\r \n BEGIN:VEVENT\r \n END:VEVENT\r \n END:VCALENDAR" ,
301+ )
302+ data .add_data_part ("text/plain" , b"Some plain text content" )
303+
304+ original_calendar_content = data .data_parts [1 ].content
305+
306+ provider .add_subject_to_text (data )
307+
308+ # text/calendar part should remain unchanged
309+ assert data .data_parts [1 ].content == original_calendar_content
310+ assert b"Planned Work Notification" not in data .data_parts [1 ].content
311+
312+ # text/plain part should have subject appended
313+ assert b"Planned Work Notification" in data .data_parts [2 ].content
0 commit comments