From 57234246b857759e86668d3e63bf0d286e7a0fda Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Fri, 15 Aug 2025 09:42:38 -0700 Subject: [PATCH 1/3] :recycle: ref: only fire active actions --- .../processors/delayed_workflow.py | 4 +++ .../workflow_engine/processors/workflow.py | 5 ++++ .../processors/test_delayed_workflow.py | 30 +++++++++++++++++++ .../processors/test_workflow.py | 18 +++++++++++ 4 files changed, 57 insertions(+) diff --git a/src/sentry/workflow_engine/processors/delayed_workflow.py b/src/sentry/workflow_engine/processors/delayed_workflow.py index b2d20eca54c375..6094e5fe5c41ba 100644 --- a/src/sentry/workflow_engine/processors/delayed_workflow.py +++ b/src/sentry/workflow_engine/processors/delayed_workflow.py @@ -14,6 +14,7 @@ import sentry.workflow_engine.buffer as buffer from sentry import features, nodestore, options from sentry.buffer.base import BufferField +from sentry.constants import ObjectStatus from sentry.db import models from sentry.issues.issue_occurrence import IssueOccurrence from sentry.models.group import Group @@ -749,6 +750,9 @@ def fire_actions_for_groups( ) total_actions += len(filtered_actions) + # We only want to fire active actions + filtered_actions = filtered_actions.filter(status=ObjectStatus.ACTIVE) + fire_actions(filtered_actions, detector, workflow_event_data) logger.info( diff --git a/src/sentry/workflow_engine/processors/workflow.py b/src/sentry/workflow_engine/processors/workflow.py index 955bae743fd358..890739ee5b2d33 100644 --- a/src/sentry/workflow_engine/processors/workflow.py +++ b/src/sentry/workflow_engine/processors/workflow.py @@ -10,6 +10,7 @@ from django.utils import timezone from sentry import features +from sentry.constants import ObjectStatus from sentry.models.activity import Activity from sentry.models.environment import Environment from sentry.services.eventstore.models import GroupEvent @@ -465,6 +466,10 @@ def process_workflows( create_workflow_fire_histories( detector, actions, event_data, should_trigger_actions, is_delayed=False ) + + # We only want to fire active actions + actions = actions.filter(status=ObjectStatus.ACTIVE) + fire_actions(actions, detector, event_data) return triggered_workflows diff --git a/tests/sentry/workflow_engine/processors/test_delayed_workflow.py b/tests/sentry/workflow_engine/processors/test_delayed_workflow.py index 6cf0d6637a9923..4ead4fe3b748fe 100644 --- a/tests/sentry/workflow_engine/processors/test_delayed_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_delayed_workflow.py @@ -6,6 +6,7 @@ from django.utils import timezone from sentry import buffer +from sentry.constants import ObjectStatus from sentry.grouping.grouptype import ErrorGroupType from sentry.models.environment import Environment from sentry.models.group import Group @@ -972,6 +973,35 @@ def test_fire_actions_for_groups__workflow_fire_history(self, mock_process: Magi event_id=self.event1.event_id, ).exists() + @patch("sentry.workflow_engine.processors.delayed_workflow.fire_actions") + def test_fire_actions_for_groups__filters_inactive_actions( + self, mock_fire_actions: MagicMock + ) -> None: + dcg_active, active_action = self.create_workflow_action(workflow=self.workflow1) + dcg_inactive, inactive_action = self.create_workflow_action(workflow=self.workflow1) + + inactive_action.update(status=ObjectStatus.DISABLED) + + test_groups_to_dcgs = { + self.group1.id: {dcg_active, dcg_inactive}, + } + test_group_to_groupevent = { + self.group1: self.event1.for_group(self.group1), + } + + fire_actions_for_groups( + self.project.organization, + test_groups_to_dcgs, + test_group_to_groupevent, + ) + + mock_fire_actions.assert_called_once() + called_actions = mock_fire_actions.call_args[0][0] + + action_ids = list(called_actions.values_list("id", flat=True)) + assert active_action.id in action_ids + assert inactive_action.id not in action_ids + class TestCleanupRedisBuffer(TestDelayedWorkflowBase): def test_cleanup_redis(self) -> None: diff --git a/tests/sentry/workflow_engine/processors/test_workflow.py b/tests/sentry/workflow_engine/processors/test_workflow.py index eb0ec04e0ee952..3b755f0c6e8632 100644 --- a/tests/sentry/workflow_engine/processors/test_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_workflow.py @@ -4,6 +4,7 @@ import pytest from django.utils import timezone +from sentry.constants import ObjectStatus from sentry.eventstream.base import GroupState from sentry.grouping.grouptype import ErrorGroupType from sentry.models.activity import Activity @@ -313,6 +314,23 @@ def test_defaults_to_error_workflows(self) -> None: triggered_workflows = process_workflows(self.event_data) assert triggered_workflows == {self.error_workflow} + @patch("sentry.workflow_engine.processors.workflow.fire_actions") + def test_filters_inactive_actions(self, mock_fire_actions: MagicMock) -> None: + _, active_action = self.create_workflow_action(workflow=self.error_workflow) + _, inactive_action = self.create_workflow_action(workflow=self.error_workflow) + + # Set the inactive action to disabled status + inactive_action.update(status=ObjectStatus.DISABLED) + + process_workflows(self.event_data) + + mock_fire_actions.assert_called_once() + called_actions = mock_fire_actions.call_args[0][0] + + action_ids = list(called_actions.values_list("id", flat=True)) + assert active_action.id in action_ids + assert inactive_action.id not in action_ids + @mock_redis_buffer() class TestEvaluateWorkflowTriggers(BaseWorkflowTest): From 4a377ae16ad62a7e3c74191df79c6cc1c66d1ded Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Fri, 15 Aug 2025 09:46:41 -0700 Subject: [PATCH 2/3] :wrench: chore: add action object manager --- src/sentry/workflow_engine/models/action.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/sentry/workflow_engine/models/action.py b/src/sentry/workflow_engine/models/action.py index 89637d4fa5e071..2cf4f08a3ada76 100644 --- a/src/sentry/workflow_engine/models/action.py +++ b/src/sentry/workflow_engine/models/action.py @@ -4,7 +4,7 @@ import logging from dataclasses import asdict from enum import StrEnum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from django.db import models from django.db.models.signals import pre_save @@ -16,6 +16,8 @@ from sentry.db.models import DefaultFieldsModel, region_silo_model, sane_repr from sentry.db.models.fields.bounded import BoundedPositiveIntegerField from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey +from sentry.db.models.manager.base import BaseManager +from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.utils import metrics from sentry.workflow_engine.models.json_config import JSONConfigBase from sentry.workflow_engine.registry import action_handler_registry @@ -28,6 +30,15 @@ logger = logging.getLogger(__name__) +class ActionManager(BaseManager["Action"]): + def get_queryset(self) -> BaseQuerySet[Action]: + return ( + super() + .get_queryset() + .exclude(status__in=(ObjectStatus.PENDING_DELETION, ObjectStatus.DELETION_IN_PROGRESS)) + ) + + @region_silo_model class Action(DefaultFieldsModel, JSONConfigBase): """ @@ -41,6 +52,9 @@ class Action(DefaultFieldsModel, JSONConfigBase): __relocation_scope__ = RelocationScope.Excluded __repr__ = sane_repr("id", "type") + objects: ClassVar[ActionManager] = ActionManager() + objects_for_deletion: ClassVar[BaseManager] = BaseManager() + class Type(StrEnum): SLACK = "slack" MSTEAMS = "msteams" From ce261cc9d9d21b798debba922627d6ee6f1d0293 Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Fri, 15 Aug 2025 14:39:43 -0700 Subject: [PATCH 3/3] :recycle: ref: pr feedback --- .../workflow_engine/processors/action.py | 12 ++-- .../processors/delayed_workflow.py | 4 -- .../workflow_engine/processors/workflow.py | 4 -- .../processors/test_action_deduplication.py | 66 +++++++++++++------ .../processors/test_delayed_workflow.py | 30 --------- .../processors/test_workflow.py | 18 ----- 6 files changed, 53 insertions(+), 81 deletions(-) diff --git a/src/sentry/workflow_engine/processors/action.py b/src/sentry/workflow_engine/processors/action.py index fdbba8f73be7bc..41da1081289444 100644 --- a/src/sentry/workflow_engine/processors/action.py +++ b/src/sentry/workflow_engine/processors/action.py @@ -123,16 +123,19 @@ def update_workflow_action_group_statuses( ) -def deduplicate_actions( +def get_unique_active_actions( actions_queryset: BaseQuerySet[Action], # decorated with the workflow_ids ) -> BaseQuerySet[Action]: """ - Deduplicates actions based on their handler's dedup_key method. - Returns a de-duplicated queryset of actions. + Returns a queryset of unique active actions based on their handler's dedup_key method. """ dedup_key_to_action_id: dict[str, int] = {} for action in actions_queryset: + # We only want to fire active actions + if action.status != ObjectStatus.ACTIVE: + continue + # workflow_id is annotated in the queryset workflow_id = getattr(action, "workflow_id") dedup_key = action.get_dedup_key(workflow_id) @@ -144,7 +147,8 @@ def deduplicate_actions( def fire_actions( actions: BaseQuerySet[Action], detector: Detector, event_data: WorkflowEventData ) -> None: - deduped_actions = deduplicate_actions(actions) + deduped_actions = get_unique_active_actions(actions) + for action in deduped_actions: task_params = build_trigger_action_task_params(action, detector, event_data) trigger_action.apply_async(kwargs=task_params, headers={"sentry-propagate-traces": False}) diff --git a/src/sentry/workflow_engine/processors/delayed_workflow.py b/src/sentry/workflow_engine/processors/delayed_workflow.py index 6094e5fe5c41ba..b2d20eca54c375 100644 --- a/src/sentry/workflow_engine/processors/delayed_workflow.py +++ b/src/sentry/workflow_engine/processors/delayed_workflow.py @@ -14,7 +14,6 @@ import sentry.workflow_engine.buffer as buffer from sentry import features, nodestore, options from sentry.buffer.base import BufferField -from sentry.constants import ObjectStatus from sentry.db import models from sentry.issues.issue_occurrence import IssueOccurrence from sentry.models.group import Group @@ -750,9 +749,6 @@ def fire_actions_for_groups( ) total_actions += len(filtered_actions) - # We only want to fire active actions - filtered_actions = filtered_actions.filter(status=ObjectStatus.ACTIVE) - fire_actions(filtered_actions, detector, workflow_event_data) logger.info( diff --git a/src/sentry/workflow_engine/processors/workflow.py b/src/sentry/workflow_engine/processors/workflow.py index 890739ee5b2d33..e80b5ed4aaa12f 100644 --- a/src/sentry/workflow_engine/processors/workflow.py +++ b/src/sentry/workflow_engine/processors/workflow.py @@ -10,7 +10,6 @@ from django.utils import timezone from sentry import features -from sentry.constants import ObjectStatus from sentry.models.activity import Activity from sentry.models.environment import Environment from sentry.services.eventstore.models import GroupEvent @@ -467,9 +466,6 @@ def process_workflows( detector, actions, event_data, should_trigger_actions, is_delayed=False ) - # We only want to fire active actions - actions = actions.filter(status=ObjectStatus.ACTIVE) - fire_actions(actions, detector, event_data) return triggered_workflows diff --git a/tests/sentry/workflow_engine/processors/test_action_deduplication.py b/tests/sentry/workflow_engine/processors/test_action_deduplication.py index 4b89110fa0457f..3ca88236711e16 100644 --- a/tests/sentry/workflow_engine/processors/test_action_deduplication.py +++ b/tests/sentry/workflow_engine/processors/test_action_deduplication.py @@ -1,11 +1,12 @@ from django.db import models from django.db.models import Value +from sentry.constants import ObjectStatus from sentry.notifications.models.notificationaction import ActionTarget from sentry.testutils.cases import TestCase from sentry.testutils.silo import region_silo_test from sentry.workflow_engine.models import Action -from sentry.workflow_engine.processors.action import deduplicate_actions +from sentry.workflow_engine.processors.action import get_unique_active_actions from sentry.workflow_engine.typings.notification_action import SentryAppIdentifier @@ -68,7 +69,7 @@ def test_deduplicate_actions_different_types(self) -> None: id__in=[self.slack_action.id, email_action.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they're different types result_ids = list(result.values_list("id", flat=True)) @@ -76,6 +77,29 @@ def test_deduplicate_actions_different_types(self) -> None: assert self.slack_action.id in result_ids assert email_action.id in result_ids + def test_deduplicate_actions_inactive_actions(self) -> None: + """Test that inactive actions are not deduplicated.""" + email_action = self.create_action( + type=Action.Type.EMAIL, + config={ + "target_type": ActionTarget.SPECIFIC, + "target_identifier": "test@example.com", + }, + status=ObjectStatus.DISABLED, + ) + + actions_queryset = Action.objects.filter( + id__in=[self.slack_action.id, email_action.id] + ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) + + result = get_unique_active_actions(actions_queryset) + + # Only one action should remain + # The inactive action should be filtered out + result_ids = list(result.values_list("id", flat=True)) + assert len(result_ids) == 1 + assert self.slack_action.id in result_ids + def test_deduplicate_actions_same_slack_channels(self) -> None: """Test that Slack actions to the same channel are deduplicated.""" slack_action_1 = self.slack_action @@ -93,7 +117,7 @@ def test_deduplicate_actions_same_slack_channels(self) -> None: id__in=[slack_action_1.id, slack_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only one action should remain result_ids = list(result.values_list("id", flat=True)) @@ -122,7 +146,7 @@ def test_deduplicate_actions_different_slack_channels(self) -> None: id__in=[slack_action_1.id, slack_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they target different channels result_ids = list(result.values_list("id", flat=True)) @@ -148,7 +172,7 @@ def test_deduplicate_multiple_slack_actions_same_channel_different_name(self) -> id__in=[slack_action_1.id, slack_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only one action should remain result_ids = list(result.values_list("id", flat=True)) @@ -182,7 +206,7 @@ def test_deduplicate_actions_same_slack_different_data(self) -> None: id__in=[slack_action_1.id, slack_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they have different data result_ids = list(result.values_list("id", flat=True)) @@ -219,7 +243,7 @@ def test_deduplicate_actions_different_slack_integrations(self) -> None: id__in=[slack_action_1.id, slack_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they have different data result_ids = list(result.values_list("id", flat=True)) @@ -250,7 +274,7 @@ def test_deduplicate_actions_email_same_target(self) -> None: id__in=[email_action_1.id, email_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only one action should remain result_ids = list(result.values_list("id", flat=True)) @@ -278,7 +302,7 @@ def test_deduplicate_actions_email_different_target_identifier(self) -> None: id__in=[email_action_1.id, email_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they have different targets result_ids = list(result.values_list("id", flat=True)) @@ -308,7 +332,7 @@ def test_deduplicate_actions_email_different_target_type(self) -> None: id__in=[email_action_1.id, email_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they have different targets result_ids = list(result.values_list("id", flat=True)) @@ -344,7 +368,7 @@ def test_deduplicate_actions_email_different_fallthrough_type(self) -> None: id__in=[email_action_1.id, email_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they have different targets result_ids = list(result.values_list("id", flat=True)) @@ -379,7 +403,7 @@ def test_deduplicate_actions_email_everything_is_same(self) -> None: id__in=[email_action_1.id, email_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only one action should remain result_ids = list(result.values_list("id", flat=True)) @@ -410,7 +434,7 @@ def test_deduplicate_actions_sentry_app_same_identifier(self) -> None: id__in=[sentry_app_action_1.id, sentry_app_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only one action should remain result_ids = list(result.values_list("id", flat=True)) @@ -437,7 +461,7 @@ def test_deduplicate_actions_webhook_same_target_identifier(self) -> None: id__in=[webhook_action_1.id, webhook_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only one action should remain result_ids = list(result.values_list("id", flat=True)) @@ -452,7 +476,7 @@ def test_deduplicate_actions_plugin_actions(self) -> None: id__in=[plugin_action_1.id, plugin_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # One action should remain since its a plugin action result_ids = list(result.values_list("id", flat=True)) @@ -484,7 +508,7 @@ def test_deduplicate_actions_mixed_types_integration_bucket(self) -> None: id__in=[slack_action.id, pagerduty_action.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they're for different integrations result_ids = list(result.values_list("id", flat=True)) @@ -521,7 +545,7 @@ def test_deduplicate_actions_ticketing_actions(self) -> None: id__in=[jira_action_1.id, jira_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since ticketing actions are deduplicated by integration_id and dynamic form field data result_ids = list(result.values_list("id", flat=True)) @@ -557,7 +581,7 @@ def test_deduplicate_actions_ticketing_actions_same_integration_and_data(self) - id__in=[jira_action_1.id, jira_action_2.id] ).annotate(workflow_id=Value(1, output_field=models.IntegerField())) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Only 1 action should remain result_ids = list(result.values_list("id", flat=True)) @@ -567,7 +591,7 @@ def test_deduplicate_actions_empty_queryset(self) -> None: """Test deduplication with empty queryset.""" actions_queryset = Action.objects.none() - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Should return empty queryset assert list(result) == [] @@ -580,7 +604,7 @@ def test_deduplicate_actions_single_action(self) -> None: workflow_id=Value(1, output_field=models.IntegerField()) ) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Should return the single action result_ids = list(result.values_list("id", flat=True)) @@ -621,7 +645,7 @@ def test_deduplicate_actions_same_actions_different_workflows(self) -> None: ) ) - result = deduplicate_actions(actions_queryset) + result = get_unique_active_actions(actions_queryset) # Both actions should remain since they're from different workflows result_ids = list(result.values_list("id", flat=True)) diff --git a/tests/sentry/workflow_engine/processors/test_delayed_workflow.py b/tests/sentry/workflow_engine/processors/test_delayed_workflow.py index 4ead4fe3b748fe..6cf0d6637a9923 100644 --- a/tests/sentry/workflow_engine/processors/test_delayed_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_delayed_workflow.py @@ -6,7 +6,6 @@ from django.utils import timezone from sentry import buffer -from sentry.constants import ObjectStatus from sentry.grouping.grouptype import ErrorGroupType from sentry.models.environment import Environment from sentry.models.group import Group @@ -973,35 +972,6 @@ def test_fire_actions_for_groups__workflow_fire_history(self, mock_process: Magi event_id=self.event1.event_id, ).exists() - @patch("sentry.workflow_engine.processors.delayed_workflow.fire_actions") - def test_fire_actions_for_groups__filters_inactive_actions( - self, mock_fire_actions: MagicMock - ) -> None: - dcg_active, active_action = self.create_workflow_action(workflow=self.workflow1) - dcg_inactive, inactive_action = self.create_workflow_action(workflow=self.workflow1) - - inactive_action.update(status=ObjectStatus.DISABLED) - - test_groups_to_dcgs = { - self.group1.id: {dcg_active, dcg_inactive}, - } - test_group_to_groupevent = { - self.group1: self.event1.for_group(self.group1), - } - - fire_actions_for_groups( - self.project.organization, - test_groups_to_dcgs, - test_group_to_groupevent, - ) - - mock_fire_actions.assert_called_once() - called_actions = mock_fire_actions.call_args[0][0] - - action_ids = list(called_actions.values_list("id", flat=True)) - assert active_action.id in action_ids - assert inactive_action.id not in action_ids - class TestCleanupRedisBuffer(TestDelayedWorkflowBase): def test_cleanup_redis(self) -> None: diff --git a/tests/sentry/workflow_engine/processors/test_workflow.py b/tests/sentry/workflow_engine/processors/test_workflow.py index 3b755f0c6e8632..eb0ec04e0ee952 100644 --- a/tests/sentry/workflow_engine/processors/test_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_workflow.py @@ -4,7 +4,6 @@ import pytest from django.utils import timezone -from sentry.constants import ObjectStatus from sentry.eventstream.base import GroupState from sentry.grouping.grouptype import ErrorGroupType from sentry.models.activity import Activity @@ -314,23 +313,6 @@ def test_defaults_to_error_workflows(self) -> None: triggered_workflows = process_workflows(self.event_data) assert triggered_workflows == {self.error_workflow} - @patch("sentry.workflow_engine.processors.workflow.fire_actions") - def test_filters_inactive_actions(self, mock_fire_actions: MagicMock) -> None: - _, active_action = self.create_workflow_action(workflow=self.error_workflow) - _, inactive_action = self.create_workflow_action(workflow=self.error_workflow) - - # Set the inactive action to disabled status - inactive_action.update(status=ObjectStatus.DISABLED) - - process_workflows(self.event_data) - - mock_fire_actions.assert_called_once() - called_actions = mock_fire_actions.call_args[0][0] - - action_ids = list(called_actions.values_list("id", flat=True)) - assert active_action.id in action_ids - assert inactive_action.id not in action_ids - @mock_redis_buffer() class TestEvaluateWorkflowTriggers(BaseWorkflowTest):