firefighter-incident 0.0.22__py3-none-any.whl → 0.0.24__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 (51) 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/confluence/models.py +66 -6
  5. firefighter/confluence/service.py +1 -1
  6. firefighter/confluence/signals/incident_updated.py +8 -26
  7. firefighter/firefighter/settings/components/jira_app.py +33 -0
  8. firefighter/incidents/admin.py +3 -0
  9. firefighter/incidents/models/impact.py +3 -5
  10. firefighter/incidents/models/incident.py +24 -9
  11. firefighter/incidents/views/views.py +2 -0
  12. firefighter/jira_app/admin.py +15 -1
  13. firefighter/jira_app/apps.py +3 -0
  14. firefighter/jira_app/client.py +151 -3
  15. firefighter/jira_app/management/__init__.py +1 -0
  16. firefighter/jira_app/management/commands/__init__.py +1 -0
  17. firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
  18. firefighter/jira_app/models.py +50 -0
  19. firefighter/jira_app/service_postmortem.py +292 -0
  20. firefighter/jira_app/signals/__init__.py +10 -0
  21. firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
  22. firefighter/jira_app/signals/postmortem_created.py +155 -0
  23. firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
  24. firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
  25. firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
  26. firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
  27. firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
  28. firefighter/raid/signals/incident_updated.py +31 -11
  29. firefighter/slack/messages/slack_messages.py +39 -3
  30. firefighter/slack/signals/postmortem_created.py +51 -3
  31. firefighter/slack/views/modals/closure_reason.py +15 -0
  32. firefighter/slack/views/modals/key_event_message.py +9 -0
  33. firefighter/slack/views/modals/postmortem.py +32 -40
  34. firefighter/slack/views/modals/update_status.py +7 -1
  35. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.24.dist-info}/METADATA +1 -1
  36. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.24.dist-info}/RECORD +51 -32
  37. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.24.dist-info}/WHEEL +1 -1
  38. firefighter_tests/test_api/test_renderer.py +41 -0
  39. firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
  40. firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
  41. firefighter_tests/test_jira_app/test_models.py +138 -0
  42. firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
  43. firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
  44. firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
  45. firefighter_tests/test_raid/test_raid_signals.py +50 -8
  46. firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
  47. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
  48. firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
  49. firefighter_tests/test_slack/views/modals/test_update_status.py +161 -129
  50. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.24.dist-info}/entry_points.txt +0 -0
  51. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.24.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, Priority
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,31 +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
- # Get P3 priority explicitly to ensure no postmortem is needed
93
- p3_priority = Priority.objects.get(value=3)
94
- 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(
95
125
  _status=IncidentStatus.MITIGATED,
96
126
  created_by=user,
97
127
  priority=p3_priority,
98
128
  )
99
- incident.save()
100
- # Mock can_be_closed to return False with MISSING_REQUIRED_KEY_EVENTS reason
101
- mocker.patch.object(
102
- type(incident),
103
- "can_be_closed",
104
- new_callable=PropertyMock,
105
- return_value=(False, [("MISSING_REQUIRED_KEY_EVENTS", "Missing key events: detected, started")])
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}"
134
+ )
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}"
106
137
  )
107
138
 
108
139
  modal = UpdateStatusModal()
@@ -111,13 +142,13 @@ class TestUpdateStatusModal:
111
142
  )
112
143
 
113
144
  ack = MagicMock()
114
- user = UserFactory.build()
115
- user.save()
116
145
 
117
146
  # Create a submission trying to close the incident
118
147
  submission_copy = dict(valid_submission)
119
148
  # Change status to CLOSED (60)
120
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
149
+ submission_copy["view"]["state"]["values"]["status"]["status"][
150
+ "selected_option"
151
+ ] = {
121
152
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
122
153
  "value": "60",
123
154
  }
@@ -129,44 +160,84 @@ class TestUpdateStatusModal:
129
160
  )
130
161
 
131
162
  # Assert that ack was called with errors (may be 1 or 2 calls depending on form validation)
132
- assert ack.called
163
+ assert ack.called, "ack should have been called"
133
164
  # Check the last call (the error response)
134
165
  last_call_kwargs = ack.call_args.kwargs
135
- assert "response_action" in last_call_kwargs
136
- assert last_call_kwargs["response_action"] == "errors"
137
- assert "errors" in last_call_kwargs
138
- 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
+ )
139
178
  # Check that the error message mentions the missing key events
140
179
  error_msg = last_call_kwargs["errors"]["status"]
141
- assert "Cannot close this incident" in error_msg
142
- 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
+ )
143
186
 
144
187
  # Verify that incident update was NOT triggered
145
188
  trigger_incident_workflow.assert_not_called()
146
189
 
147
190
  @staticmethod
148
- 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:
149
194
  """Test that closing from POST_MORTEM is prevented when key events missing.
150
195
 
151
196
  This tests a P1/P2 incident in POST_MORTEM trying to close but blocked
152
197
  by missing key events.
198
+
199
+ This test does NOT mock can_be_closed - it uses real milestone validation.
153
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
+
154
223
  # Create a user first
155
224
  user = UserFactory.build()
156
225
  user.save()
157
226
 
158
227
  # Create a P1/P2 incident in POST_MORTEM status
159
- incident = IncidentFactory.build(
228
+ # This incident will have missing milestones (detected, started)
229
+ incident = IncidentFactory.create(
160
230
  _status=IncidentStatus.POST_MORTEM,
161
231
  created_by=user,
162
232
  )
163
- incident.save()
164
- # Mock can_be_closed to return False with MISSING_REQUIRED_KEY_EVENTS reason
165
- mocker.patch.object(
166
- type(incident),
167
- "can_be_closed",
168
- new_callable=PropertyMock,
169
- 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}"
170
241
  )
171
242
 
172
243
  modal = UpdateStatusModal()
@@ -175,12 +246,12 @@ class TestUpdateStatusModal:
175
246
  )
176
247
 
177
248
  ack = MagicMock()
178
- user = UserFactory.build()
179
- user.save()
180
249
 
181
250
  # Create a submission trying to close the incident
182
251
  submission_copy = dict(valid_submission)
183
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
252
+ submission_copy["view"]["state"]["values"]["status"]["status"][
253
+ "selected_option"
254
+ ] = {
184
255
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
185
256
  "value": "60",
186
257
  }
@@ -191,97 +262,35 @@ class TestUpdateStatusModal:
191
262
  )
192
263
 
193
264
  # Assert that ack was called with errors
194
- assert ack.called
265
+ assert ack.called, "ack should have been called"
195
266
  last_call_kwargs = ack.call_args.kwargs
196
- assert "response_action" in last_call_kwargs
197
- assert last_call_kwargs["response_action"] == "errors"
198
- assert "errors" in last_call_kwargs
199
- assert "status" in last_call_kwargs["errors"]
200
- error_msg = last_call_kwargs["errors"]["status"]
201
- assert "Cannot close this incident" in error_msg
202
- assert "Missing key events" in error_msg
203
-
204
- # Verify that incident update was NOT triggered
205
- trigger_incident_workflow.assert_not_called()
206
-
207
- @staticmethod
208
- def test_cannot_close_p1_p2_without_postmortem(mocker: MockerFixture, priority_factory, environment_factory) -> None:
209
- """Test that P1/P2 incidents in PRD cannot be closed directly from INVESTIGATING.
210
-
211
- For P1/P2 incidents requiring post-mortem, although the form allows CLOSED as an option
212
- from INVESTIGATING status, the can_be_closed validation should prevent closure with
213
- an error message about needing to go through post-mortem.
214
- """
215
- # Create a user first
216
- user = UserFactory.build()
217
- user.save()
218
-
219
- # Create P1 priority (needs_postmortem=True) and PRD environment
220
- p1_priority = priority_factory(value=1, name="P1", needs_postmortem=True)
221
- prd_environment = environment_factory(value="PRD", name="Production")
222
-
223
- # Create a P1/P2 incident in INVESTIGATING status
224
- # From INVESTIGATING, the form allows transitioning to CLOSED (but can_be_closed will block it)
225
- incident = IncidentFactory.build(
226
- _status=IncidentStatus.INVESTIGATING,
227
- created_by=user,
228
- priority=p1_priority,
229
- environment=prd_environment,
230
- )
231
- incident.save()
232
- # Mock can_be_closed to return False with STATUS_NOT_POST_MORTEM reason
233
- mocker.patch.object(
234
- type(incident),
235
- "can_be_closed",
236
- new_callable=PropertyMock,
237
- return_value=(False, [("STATUS_NOT_POST_MORTEM", "Incident is not in PostMortem status, and needs one because of its priority and environment (P1/PRD).")])
267
+ assert "response_action" in last_call_kwargs, (
268
+ f"Expected 'response_action' in ack call, got: {last_call_kwargs}"
238
269
  )
239
-
240
- modal = UpdateStatusModal()
241
-
242
- # Mock handle_update_status_close_request to NOT show closure reason modal
243
- # This allows the test to reach the can_be_closed validation
244
- mocker.patch(
245
- "firefighter.slack.views.modals.update_status.handle_update_status_close_request",
246
- return_value=False
270
+ assert last_call_kwargs["response_action"] == "errors", (
271
+ f"Expected response_action='errors', got: {last_call_kwargs.get('response_action')}"
247
272
  )
248
-
249
- trigger_incident_workflow = mocker.patch.object(
250
- modal, "_trigger_incident_workflow"
273
+ assert "errors" in last_call_kwargs, (
274
+ f"Expected 'errors' in ack call, got: {last_call_kwargs}"
251
275
  )
252
-
253
- ack = MagicMock()
254
- user = UserFactory.build()
255
- user.save()
256
-
257
- # Create a submission trying to close the incident
258
- submission_copy = dict(valid_submission)
259
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
260
- "text": {"type": "plain_text", "text": "Closed", "emoji": True},
261
- "value": "60",
262
- }
263
- submission_copy["view"]["private_metadata"] = str(incident.id)
264
-
265
- modal.handle_modal_fn(
266
- ack=ack, body=submission_copy, incident=incident, user=user
276
+ assert "status" in last_call_kwargs["errors"], (
277
+ f"Expected 'status' in errors, got: {last_call_kwargs.get('errors')}"
267
278
  )
268
-
269
- # Assert that ack was called with errors
270
- assert ack.called
271
- last_call_kwargs = ack.call_args.kwargs
272
- assert "response_action" in last_call_kwargs
273
- assert last_call_kwargs["response_action"] == "errors"
274
- assert "errors" in last_call_kwargs
275
- assert "status" in last_call_kwargs["errors"]
276
279
  error_msg = last_call_kwargs["errors"]["status"]
277
- assert "Cannot close this incident" in error_msg
278
- 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
+ )
279
286
 
280
287
  # Verify that incident update was NOT triggered
281
288
  trigger_incident_workflow.assert_not_called()
282
289
 
283
290
  @staticmethod
284
- 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:
285
294
  """Test that closure reason modal is shown when trying to close from INVESTIGATING.
286
295
 
287
296
  This tests that handle_update_status_close_request correctly shows the
@@ -297,7 +306,7 @@ class TestUpdateStatusModal:
297
306
  # Mock handle_update_status_close_request to return True (modal shown)
298
307
  mock_handle_close = mocker.patch(
299
308
  "firefighter.slack.views.modals.update_status.handle_update_status_close_request",
300
- return_value=True
309
+ return_value=True,
301
310
  )
302
311
 
303
312
  trigger_incident_workflow = mocker.patch.object(
@@ -310,7 +319,9 @@ class TestUpdateStatusModal:
310
319
 
311
320
  # Create a submission trying to close the incident
312
321
  submission_copy = dict(valid_submission)
313
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
322
+ submission_copy["view"]["state"]["values"]["status"]["status"][
323
+ "selected_option"
324
+ ] = {
314
325
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
315
326
  "value": "60",
316
327
  }
@@ -321,13 +332,17 @@ class TestUpdateStatusModal:
321
332
  )
322
333
 
323
334
  # Verify handle_update_status_close_request was called
324
- 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
+ )
325
338
 
326
339
  # Verify that incident update was NOT triggered (because closure reason modal was shown)
327
340
  trigger_incident_workflow.assert_not_called()
328
341
 
329
342
  @staticmethod
330
- 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:
331
346
  """Test that closing is allowed when all conditions are met for P3+ incidents."""
332
347
  # Create a user first
333
348
  user = UserFactory.build()
@@ -352,7 +367,7 @@ class TestUpdateStatusModal:
352
367
  type(incident),
353
368
  "can_be_closed",
354
369
  new_callable=PropertyMock,
355
- return_value=(True, [])
370
+ return_value=(True, []),
356
371
  )
357
372
 
358
373
  modal = UpdateStatusModal()
@@ -364,7 +379,9 @@ class TestUpdateStatusModal:
364
379
 
365
380
  # Create a submission to close the incident
366
381
  submission_copy = dict(valid_submission)
367
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
382
+ submission_copy["view"]["state"]["values"]["status"]["status"][
383
+ "selected_option"
384
+ ] = {
368
385
  "text": {"type": "plain_text", "text": "Closed", "emoji": True},
369
386
  "value": "60",
370
387
  }
@@ -376,14 +393,18 @@ class TestUpdateStatusModal:
376
393
 
377
394
  # Assert that ack was called successfully (no errors)
378
395
  # The first call is the successful ack() without errors
379
- 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
+ )
380
399
  assert first_call_kwargs == {} or "errors" not in first_call_kwargs
381
400
 
382
401
  # Verify that incident update WAS triggered
383
402
  trigger_incident_workflow.assert_called_once()
384
403
 
385
404
  @staticmethod
386
- 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:
387
408
  """Test that priority can be updated without changing status.
388
409
 
389
410
  This reproduces the bug where trying to update only the priority of a P4
@@ -420,12 +441,16 @@ class TestUpdateStatusModal:
420
441
  # The status field will have MITIGATED (40) as initial value, but it's not in the available choices
421
442
  submission_copy = dict(valid_submission)
422
443
  # Status unchanged - keeps MITIGATED (40)
423
- submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
444
+ submission_copy["view"]["state"]["values"]["status"]["status"][
445
+ "selected_option"
446
+ ] = {
424
447
  "text": {"type": "plain_text", "text": "Mitigated", "emoji": True},
425
448
  "value": "40", # This should cause validation error with current code
426
449
  }
427
450
  # Change priority to P4
428
- submission_copy["view"]["state"]["values"]["priority"]["priority"]["selected_option"] = {
451
+ submission_copy["view"]["state"]["values"]["priority"]["priority"][
452
+ "selected_option"
453
+ ] = {
429
454
  "text": {"type": "plain_text", "text": "P4", "emoji": True},
430
455
  "value": str(p4_priority.id),
431
456
  }
@@ -437,9 +462,12 @@ class TestUpdateStatusModal:
437
462
 
438
463
  # Assert that ack was called successfully WITHOUT errors
439
464
  # With the bug, this would fail with "Select a valid choice. 40 is not one of the available choices"
440
- first_call_kwargs = ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
441
- 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, (
442
469
  f"Should allow updating priority without changing status. Got errors: {first_call_kwargs.get('errors')}"
470
+ )
443
471
 
444
472
  # Verify that incident update WAS triggered (priority changed)
445
473
  trigger_incident_workflow.assert_called_once()
@@ -639,7 +667,11 @@ valid_submission = {
639
667
  {
640
668
  "type": "input",
641
669
  "block_id": "incident_category",
642
- "label": {"type": "plain_text", "text": "Issue category", "emoji": True},
670
+ "label": {
671
+ "type": "plain_text",
672
+ "text": "Issue category",
673
+ "emoji": True,
674
+ },
643
675
  "optional": False,
644
676
  "dispatch_action": False,
645
677
  "element": {