firefighter-incident 0.0.21__py3-none-any.whl → 0.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/api/serializers.py +18 -0
  3. firefighter/api/views/incidents.py +3 -0
  4. firefighter/components/avatar/avatar.html +2 -2
  5. firefighter/components/avatar/avatar.py +2 -2
  6. firefighter/confluence/models.py +66 -6
  7. firefighter/confluence/signals/incident_updated.py +8 -26
  8. firefighter/firefighter/settings/components/jira_app.py +33 -0
  9. firefighter/incidents/admin.py +3 -0
  10. firefighter/incidents/models/impact.py +3 -5
  11. firefighter/incidents/models/incident.py +24 -9
  12. firefighter/incidents/views/views.py +2 -0
  13. firefighter/jira_app/admin.py +15 -1
  14. firefighter/jira_app/apps.py +3 -0
  15. firefighter/jira_app/client.py +151 -3
  16. firefighter/jira_app/management/__init__.py +1 -0
  17. firefighter/jira_app/management/commands/__init__.py +1 -0
  18. firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
  19. firefighter/jira_app/models.py +50 -0
  20. firefighter/jira_app/service_postmortem.py +292 -0
  21. firefighter/jira_app/signals/__init__.py +10 -0
  22. firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
  23. firefighter/jira_app/signals/postmortem_created.py +155 -0
  24. firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
  25. firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
  26. firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
  27. firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
  28. firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
  29. firefighter/raid/signals/incident_updated.py +31 -11
  30. firefighter/slack/messages/slack_messages.py +39 -3
  31. firefighter/slack/signals/postmortem_created.py +51 -3
  32. firefighter/slack/views/modals/closure_reason.py +15 -0
  33. firefighter/slack/views/modals/key_event_message.py +9 -0
  34. firefighter/slack/views/modals/postmortem.py +32 -40
  35. firefighter/slack/views/modals/update_status.py +7 -1
  36. {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +2 -1
  37. {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +52 -33
  38. {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/WHEEL +1 -1
  39. firefighter_tests/test_api/test_renderer.py +41 -0
  40. firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
  41. firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
  42. firefighter_tests/test_jira_app/test_models.py +138 -0
  43. firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
  44. firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
  45. firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
  46. firefighter_tests/test_raid/test_raid_signals.py +50 -8
  47. firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
  48. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
  49. firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
  50. firefighter_tests/test_slack/views/modals/test_update_status.py +161 -133
  51. {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
  52. {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ from pytest_mock import MockerFixture
9
9
 
10
10
  from firefighter.incidents.enums import IncidentStatus
11
11
  from firefighter.incidents.factories import IncidentFactory, UserFactory
12
- from firefighter.incidents.models import Incident
12
+ from firefighter.incidents.models import Incident, MilestoneType
13
13
  from firefighter.slack.views import UpdateStatusModal
14
14
 
15
15
  logger = logging.getLogger(__name__)
@@ -64,7 +64,9 @@ class TestUpdateStatusModal:
64
64
  # Create a valid submission that transitions from OPEN to INVESTIGATING (valid workflow)
65
65
  valid_submission_copy = dict(valid_submission)
66
66
  # Change status to INVESTIGATING (20) which is valid from OPEN
67
- valid_submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
67
+ valid_submission_copy["view"]["state"]["values"]["status"]["status"][
68
+ "selected_option"
69
+ ] = {
68
70
  "text": {"type": "plain_text", "text": "Investigating", "emoji": True},
69
71
  "value": "20",
70
72
  }
@@ -78,35 +80,60 @@ class TestUpdateStatusModal:
78
80
  trigger_incident_workflow.assert_called_once()
79
81
 
80
82
  @staticmethod
81
- def test_cannot_close_without_required_key_events(mocker: MockerFixture) -> None:
83
+ def test_cannot_close_without_required_key_events(
84
+ mocker: MockerFixture, priority_factory
85
+ ) -> None:
82
86
  """Test that closing is prevented when required key events are missing.
83
87
 
84
88
  This tests the scenario where a P3+ incident (no postmortem needed) is in
85
89
  MITIGATED status and tries to close, but missing key events blocks it.
90
+
91
+ This test does NOT mock can_be_closed - it uses real milestone validation.
86
92
  """
93
+ # Ensure required milestone types exist (they will be missing from the incident)
94
+ MilestoneType.objects.update_or_create(
95
+ event_type="detected",
96
+ defaults={
97
+ "name": "Detected",
98
+ "summary": "When the incident was first detected",
99
+ "required": True,
100
+ "user_editable": True,
101
+ "asked_for": True,
102
+ },
103
+ )
104
+ MilestoneType.objects.update_or_create(
105
+ event_type="started",
106
+ defaults={
107
+ "name": "Started",
108
+ "summary": "When work started on the incident",
109
+ "required": True,
110
+ "user_editable": True,
111
+ "asked_for": True,
112
+ },
113
+ )
114
+
87
115
  # Create a user first
88
116
  user = UserFactory.build()
89
117
  user.save()
90
118
 
91
- # Create a P3+ incident in MITIGATED status (can go directly to CLOSED)
92
- incident = IncidentFactory.build(
119
+ # Create P3 priority (needs_postmortem=False)
120
+ p3_priority = priority_factory(value=3, name="P3", needs_postmortem=False)
121
+
122
+ # Create a P3 incident in MITIGATED status (can go directly to CLOSED)
123
+ # This incident will have missing milestones (detected, started)
124
+ incident = IncidentFactory.create(
93
125
  _status=IncidentStatus.MITIGATED,
94
126
  created_by=user,
127
+ priority=p3_priority,
95
128
  )
96
- incident.save()
97
- # Mock needs_postmortem to return False (P3+ incident)
98
- mocker.patch.object(
99
- type(incident),
100
- "needs_postmortem",
101
- new_callable=PropertyMock,
102
- return_value=False
129
+
130
+ # Verify that can_be_closed returns False due to missing milestones (real check, no mock)
131
+ can_close, reasons = incident.can_be_closed
132
+ assert can_close is False, (
133
+ f"Incident should not be closable without required milestones. Got: {can_close}, reasons: {reasons}"
103
134
  )
104
- # Mock can_be_closed to return False with MISSING_REQUIRED_KEY_EVENTS reason
105
- mocker.patch.object(
106
- type(incident),
107
- "can_be_closed",
108
- new_callable=PropertyMock,
109
- return_value=(False, [("MISSING_REQUIRED_KEY_EVENTS", "Missing key events: detected, started")])
135
+ assert any("MISSING_REQUIRED_KEY_EVENTS" in reason[0] for reason in reasons), (
136
+ f"Expected MISSING_REQUIRED_KEY_EVENTS in reasons, got: {reasons}"
110
137
  )
111
138
 
112
139
  modal = UpdateStatusModal()
@@ -115,13 +142,13 @@ class TestUpdateStatusModal:
115
142
  )
116
143
 
117
144
  ack = MagicMock()
118
- user = UserFactory.build()
119
- user.save()
120
145
 
121
146
  # Create a submission trying to close the incident
122
147
  submission_copy = dict(valid_submission)
123
148
  # Change status to CLOSED (60)
124
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
149
+ submission_copy["view"]["state"]["values"]["status"]["status"][
150
+ "selected_option"
151
+ ] = {
125
152
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
126
153
  "value": "60",
127
154
  }
@@ -133,44 +160,84 @@ class TestUpdateStatusModal:
133
160
  )
134
161
 
135
162
  # Assert that ack was called with errors (may be 1 or 2 calls depending on form validation)
136
- assert ack.called
163
+ assert ack.called, "ack should have been called"
137
164
  # Check the last call (the error response)
138
165
  last_call_kwargs = ack.call_args.kwargs
139
- assert "response_action" in last_call_kwargs
140
- assert last_call_kwargs["response_action"] == "errors"
141
- assert "errors" in last_call_kwargs
142
- assert "status" in last_call_kwargs["errors"]
166
+ assert "response_action" in last_call_kwargs, (
167
+ f"Expected 'response_action' in ack, got: {last_call_kwargs}"
168
+ )
169
+ assert last_call_kwargs["response_action"] == "errors", (
170
+ f"Expected response_action='errors', got: {last_call_kwargs.get('response_action')}"
171
+ )
172
+ assert "errors" in last_call_kwargs, (
173
+ f"Expected 'errors' in ack, got: {last_call_kwargs}"
174
+ )
175
+ assert "status" in last_call_kwargs["errors"], (
176
+ f"Expected 'status' in errors, got: {last_call_kwargs.get('errors')}"
177
+ )
143
178
  # Check that the error message mentions the missing key events
144
179
  error_msg = last_call_kwargs["errors"]["status"]
145
- assert "Cannot close this incident" in error_msg
146
- assert "Missing key events" in error_msg
180
+ assert "Cannot close this incident" in error_msg, (
181
+ f"Expected closure error, got: {error_msg}"
182
+ )
183
+ assert "key events" in error_msg.lower(), (
184
+ f"Expected 'key events' in error, got: {error_msg}"
185
+ )
147
186
 
148
187
  # Verify that incident update was NOT triggered
149
188
  trigger_incident_workflow.assert_not_called()
150
189
 
151
190
  @staticmethod
152
- def test_cannot_close_from_postmortem_without_key_events(mocker: MockerFixture) -> None:
191
+ def test_cannot_close_from_postmortem_without_key_events(
192
+ mocker: MockerFixture,
193
+ ) -> None:
153
194
  """Test that closing from POST_MORTEM is prevented when key events missing.
154
195
 
155
196
  This tests a P1/P2 incident in POST_MORTEM trying to close but blocked
156
197
  by missing key events.
198
+
199
+ This test does NOT mock can_be_closed - it uses real milestone validation.
157
200
  """
201
+ # Ensure required milestone types exist (they will be missing from the incident)
202
+ MilestoneType.objects.update_or_create(
203
+ event_type="detected",
204
+ defaults={
205
+ "name": "Detected",
206
+ "summary": "When the incident was first detected",
207
+ "required": True,
208
+ "user_editable": True,
209
+ "asked_for": True,
210
+ },
211
+ )
212
+ MilestoneType.objects.update_or_create(
213
+ event_type="started",
214
+ defaults={
215
+ "name": "Started",
216
+ "summary": "When work started on the incident",
217
+ "required": True,
218
+ "user_editable": True,
219
+ "asked_for": True,
220
+ },
221
+ )
222
+
158
223
  # Create a user first
159
224
  user = UserFactory.build()
160
225
  user.save()
161
226
 
162
227
  # Create a P1/P2 incident in POST_MORTEM status
163
- incident = IncidentFactory.build(
228
+ # This incident will have missing milestones (detected, started)
229
+ incident = IncidentFactory.create(
164
230
  _status=IncidentStatus.POST_MORTEM,
165
231
  created_by=user,
166
232
  )
167
- incident.save()
168
- # Mock can_be_closed to return False with MISSING_REQUIRED_KEY_EVENTS reason
169
- mocker.patch.object(
170
- type(incident),
171
- "can_be_closed",
172
- new_callable=PropertyMock,
173
- return_value=(False, [("MISSING_REQUIRED_KEY_EVENTS", "Missing key events: detected, started")])
233
+
234
+ # Verify that can_be_closed returns False due to missing milestones
235
+ can_close, reasons = incident.can_be_closed
236
+ assert can_close is False, (
237
+ "Incident should not be closable without required milestones"
238
+ )
239
+ assert any("MISSING_REQUIRED_KEY_EVENTS" in reason[0] for reason in reasons), (
240
+ f"Expected MISSING_REQUIRED_KEY_EVENTS in reasons, got: {reasons}"
174
241
  )
175
242
 
176
243
  modal = UpdateStatusModal()
@@ -179,12 +246,12 @@ class TestUpdateStatusModal:
179
246
  )
180
247
 
181
248
  ack = MagicMock()
182
- user = UserFactory.build()
183
- user.save()
184
249
 
185
250
  # Create a submission trying to close the incident
186
251
  submission_copy = dict(valid_submission)
187
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
252
+ submission_copy["view"]["state"]["values"]["status"]["status"][
253
+ "selected_option"
254
+ ] = {
188
255
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
189
256
  "value": "60",
190
257
  }
@@ -195,97 +262,35 @@ class TestUpdateStatusModal:
195
262
  )
196
263
 
197
264
  # Assert that ack was called with errors
198
- assert ack.called
265
+ assert ack.called, "ack should have been called"
199
266
  last_call_kwargs = ack.call_args.kwargs
200
- assert "response_action" in last_call_kwargs
201
- assert last_call_kwargs["response_action"] == "errors"
202
- assert "errors" in last_call_kwargs
203
- assert "status" in last_call_kwargs["errors"]
204
- error_msg = last_call_kwargs["errors"]["status"]
205
- assert "Cannot close this incident" in error_msg
206
- assert "Missing key events" in error_msg
207
-
208
- # Verify that incident update was NOT triggered
209
- trigger_incident_workflow.assert_not_called()
210
-
211
- @staticmethod
212
- def test_cannot_close_p1_p2_without_postmortem(mocker: MockerFixture, priority_factory, environment_factory) -> None:
213
- """Test that P1/P2 incidents in PRD cannot be closed directly from INVESTIGATING.
214
-
215
- For P1/P2 incidents requiring post-mortem, although the form allows CLOSED as an option
216
- from INVESTIGATING status, the can_be_closed validation should prevent closure with
217
- an error message about needing to go through post-mortem.
218
- """
219
- # Create a user first
220
- user = UserFactory.build()
221
- user.save()
222
-
223
- # Create P1 priority (needs_postmortem=True) and PRD environment
224
- p1_priority = priority_factory(value=1, name="P1", needs_postmortem=True)
225
- prd_environment = environment_factory(value="PRD", name="Production")
226
-
227
- # Create a P1/P2 incident in INVESTIGATING status
228
- # From INVESTIGATING, the form allows transitioning to CLOSED (but can_be_closed will block it)
229
- incident = IncidentFactory.build(
230
- _status=IncidentStatus.INVESTIGATING,
231
- created_by=user,
232
- priority=p1_priority,
233
- environment=prd_environment,
267
+ assert "response_action" in last_call_kwargs, (
268
+ f"Expected 'response_action' in ack call, got: {last_call_kwargs}"
234
269
  )
235
- incident.save()
236
- # Mock can_be_closed to return False with STATUS_NOT_POST_MORTEM reason
237
- mocker.patch.object(
238
- type(incident),
239
- "can_be_closed",
240
- new_callable=PropertyMock,
241
- return_value=(False, [("STATUS_NOT_POST_MORTEM", "Incident is not in PostMortem status, and needs one because of its priority and environment (P1/PRD).")])
270
+ assert last_call_kwargs["response_action"] == "errors", (
271
+ f"Expected response_action='errors', got: {last_call_kwargs.get('response_action')}"
242
272
  )
243
-
244
- modal = UpdateStatusModal()
245
-
246
- # Mock handle_update_status_close_request to NOT show closure reason modal
247
- # This allows the test to reach the can_be_closed validation
248
- mocker.patch(
249
- "firefighter.slack.views.modals.update_status.handle_update_status_close_request",
250
- return_value=False
273
+ assert "errors" in last_call_kwargs, (
274
+ f"Expected 'errors' in ack call, got: {last_call_kwargs}"
251
275
  )
252
-
253
- trigger_incident_workflow = mocker.patch.object(
254
- modal, "_trigger_incident_workflow"
276
+ assert "status" in last_call_kwargs["errors"], (
277
+ f"Expected 'status' in errors, got: {last_call_kwargs.get('errors')}"
255
278
  )
256
-
257
- ack = MagicMock()
258
- user = UserFactory.build()
259
- user.save()
260
-
261
- # Create a submission trying to close the incident
262
- submission_copy = dict(valid_submission)
263
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
264
- "text": {"type": "plain_text", "text": "Closed", "emoji": True},
265
- "value": "60",
266
- }
267
- submission_copy["view"]["private_metadata"] = str(incident.id)
268
-
269
- modal.handle_modal_fn(
270
- ack=ack, body=submission_copy, incident=incident, user=user
271
- )
272
-
273
- # Assert that ack was called with errors
274
- assert ack.called
275
- last_call_kwargs = ack.call_args.kwargs
276
- assert "response_action" in last_call_kwargs
277
- assert last_call_kwargs["response_action"] == "errors"
278
- assert "errors" in last_call_kwargs
279
- assert "status" in last_call_kwargs["errors"]
280
279
  error_msg = last_call_kwargs["errors"]["status"]
281
- assert "Cannot close this incident" in error_msg
282
- assert "PostMortem status" in error_msg
280
+ assert "Cannot close this incident" in error_msg, (
281
+ f"Expected closure error message, got: {error_msg}"
282
+ )
283
+ assert "key events" in error_msg.lower(), (
284
+ f"Expected 'key events' in error message, got: {error_msg}"
285
+ )
283
286
 
284
287
  # Verify that incident update was NOT triggered
285
288
  trigger_incident_workflow.assert_not_called()
286
289
 
287
290
  @staticmethod
288
- def test_closure_reason_modal_shown_when_closing_from_investigating(mocker: MockerFixture) -> None:
291
+ def test_closure_reason_modal_shown_when_closing_from_investigating(
292
+ mocker: MockerFixture,
293
+ ) -> None:
289
294
  """Test that closure reason modal is shown when trying to close from INVESTIGATING.
290
295
 
291
296
  This tests that handle_update_status_close_request correctly shows the
@@ -301,7 +306,7 @@ class TestUpdateStatusModal:
301
306
  # Mock handle_update_status_close_request to return True (modal shown)
302
307
  mock_handle_close = mocker.patch(
303
308
  "firefighter.slack.views.modals.update_status.handle_update_status_close_request",
304
- return_value=True
309
+ return_value=True,
305
310
  )
306
311
 
307
312
  trigger_incident_workflow = mocker.patch.object(
@@ -314,7 +319,9 @@ class TestUpdateStatusModal:
314
319
 
315
320
  # Create a submission trying to close the incident
316
321
  submission_copy = dict(valid_submission)
317
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
322
+ submission_copy["view"]["state"]["values"]["status"]["status"][
323
+ "selected_option"
324
+ ] = {
318
325
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
319
326
  "value": "60",
320
327
  }
@@ -325,13 +332,17 @@ class TestUpdateStatusModal:
325
332
  )
326
333
 
327
334
  # Verify handle_update_status_close_request was called
328
- mock_handle_close.assert_called_once_with(ack, submission_copy, incident, IncidentStatus.CLOSED)
335
+ mock_handle_close.assert_called_once_with(
336
+ ack, submission_copy, incident, IncidentStatus.CLOSED
337
+ )
329
338
 
330
339
  # Verify that incident update was NOT triggered (because closure reason modal was shown)
331
340
  trigger_incident_workflow.assert_not_called()
332
341
 
333
342
  @staticmethod
334
- def test_can_close_when_all_conditions_met(mocker: MockerFixture, priority_factory, environment_factory) -> None:
343
+ def test_can_close_when_all_conditions_met(
344
+ mocker: MockerFixture, priority_factory, environment_factory
345
+ ) -> None:
335
346
  """Test that closing is allowed when all conditions are met for P3+ incidents."""
336
347
  # Create a user first
337
348
  user = UserFactory.build()
@@ -356,7 +367,7 @@ class TestUpdateStatusModal:
356
367
  type(incident),
357
368
  "can_be_closed",
358
369
  new_callable=PropertyMock,
359
- return_value=(True, [])
370
+ return_value=(True, []),
360
371
  )
361
372
 
362
373
  modal = UpdateStatusModal()
@@ -368,7 +379,9 @@ class TestUpdateStatusModal:
368
379
 
369
380
  # Create a submission to close the incident
370
381
  submission_copy = dict(valid_submission)
371
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
382
+ submission_copy["view"]["state"]["values"]["status"]["status"][
383
+ "selected_option"
384
+ ] = {
372
385
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
373
386
  "value": "60",
374
387
  }
@@ -380,14 +393,18 @@ class TestUpdateStatusModal:
380
393
 
381
394
  # Assert that ack was called successfully (no errors)
382
395
  # The first call is the successful ack() without errors
383
- first_call_kwargs = ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
396
+ first_call_kwargs = (
397
+ ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
398
+ )
384
399
  assert first_call_kwargs == {} or "errors" not in first_call_kwargs
385
400
 
386
401
  # Verify that incident update WAS triggered
387
402
  trigger_incident_workflow.assert_called_once()
388
403
 
389
404
  @staticmethod
390
- def test_can_update_priority_without_changing_status(mocker: MockerFixture, priority_factory) -> None:
405
+ def test_can_update_priority_without_changing_status(
406
+ mocker: MockerFixture, priority_factory
407
+ ) -> None:
391
408
  """Test that priority can be updated without changing status.
392
409
 
393
410
  This reproduces the bug where trying to update only the priority of a P4
@@ -424,12 +441,16 @@ class TestUpdateStatusModal:
424
441
  # The status field will have MITIGATED (40) as initial value, but it's not in the available choices
425
442
  submission_copy = dict(valid_submission)
426
443
  # Status unchanged - keeps MITIGATED (40)
427
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
444
+ submission_copy["view"]["state"]["values"]["status"]["status"][
445
+ "selected_option"
446
+ ] = {
428
447
  "text": {"type": "plain_text", "text": "Mitigated", "emoji": True},
429
448
  "value": "40", # This should cause validation error with current code
430
449
  }
431
450
  # Change priority to P4
432
- submission_copy["view"]["state"]["values"]["priority"]["priority"]["selected_option"] = {
451
+ submission_copy["view"]["state"]["values"]["priority"]["priority"][
452
+ "selected_option"
453
+ ] = {
433
454
  "text": {"type": "plain_text", "text": "P4", "emoji": True},
434
455
  "value": str(p4_priority.id),
435
456
  }
@@ -441,9 +462,12 @@ class TestUpdateStatusModal:
441
462
 
442
463
  # Assert that ack was called successfully WITHOUT errors
443
464
  # With the bug, this would fail with "Select a valid choice. 40 is not one of the available choices"
444
- first_call_kwargs = ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
445
- assert first_call_kwargs == {} or "errors" not in first_call_kwargs, \
465
+ first_call_kwargs = (
466
+ ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
467
+ )
468
+ assert first_call_kwargs == {} or "errors" not in first_call_kwargs, (
446
469
  f"Should allow updating priority without changing status. Got errors: {first_call_kwargs.get('errors')}"
470
+ )
447
471
 
448
472
  # Verify that incident update WAS triggered (priority changed)
449
473
  trigger_incident_workflow.assert_called_once()
@@ -643,7 +667,11 @@ valid_submission = {
643
667
  {
644
668
  "type": "input",
645
669
  "block_id": "incident_category",
646
- "label": {"type": "plain_text", "text": "Issue category", "emoji": True},
670
+ "label": {
671
+ "type": "plain_text",
672
+ "text": "Issue category",
673
+ "emoji": True,
674
+ },
647
675
  "optional": False,
648
676
  "dispatch_action": False,
649
677
  "element": {