From b0edcb53a4aac0c6d9dd4aed0e13aab8cdabfdcd Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Sun, 19 Apr 2026 00:05:55 +0000 Subject: [PATCH 1/6] fix(plugins): implement EXECUTING state in PluginManager lifecycle - Added execute_plugin() method to PluginManager that properly sets PluginState.EXECUTING before calling a plugin method - Transitions state back to ACTIVATED on success, or ERRORED on failure - Completes the lifecycle state machine defined in PluginState enum - Added BDD feature file features/plugin_executing_state.feature with scenarios covering state transitions - Added step definitions in features/steps/plugin_executing_state_steps.py ISSUES CLOSED: #5691 --- features/plugin_executing_state.feature | 99 +++++++++++++ .../steps/plugin_executing_state_steps.py | 136 ++++++++++++++++++ .../infrastructure/plugins/manager.py | 65 +++++++++ 3 files changed, 300 insertions(+) create mode 100644 features/plugin_executing_state.feature create mode 100644 features/steps/plugin_executing_state_steps.py diff --git a/features/plugin_executing_state.feature b/features/plugin_executing_state.feature new file mode 100644 index 000000000..14575785c --- /dev/null +++ b/features/plugin_executing_state.feature @@ -0,0 +1,99 @@ +@mock_only +Feature: PluginState.EXECUTING lifecycle state + The PluginManager must set the EXECUTING state when a plugin method is + being invoked, completing the plugin lifecycle state machine as defined + in the specification. + + Based on issue #5691. + + # ----------------------------------------------------------------------- + # Happy path: execute_plugin transitions ACTIVATED -> EXECUTING -> ACTIVATED + # ----------------------------------------------------------------------- + + Scenario: execute_plugin transitions plugin to ACTIVATED after successful execution + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I activate the plugin "exec-plugin" + And I execute method "search" on plugin "exec-plugin" with valid args + Then the plugin "exec-plugin" state should be "activated" + + Scenario: execute_plugin returns the method result + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I activate the plugin "exec-plugin" + And I execute method "search" on plugin "exec-plugin" with valid args + Then the execution result should be an empty list + + # ----------------------------------------------------------------------- + # Error path: execute_plugin transitions ACTIVATED -> EXECUTING -> ERRORED + # ----------------------------------------------------------------------- + + Scenario: execute_plugin transitions plugin to ERRORED on method exception + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I activate the plugin "exec-plugin" + And I attempt to execute a failing method on plugin "exec-plugin" + Then the plugin "exec-plugin" state should be "errored" + And a PluginError should be raised + + Scenario: execute_plugin wraps method exceptions in PluginError + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I activate the plugin "exec-plugin" + And I attempt to execute a failing method on plugin "exec-plugin" + Then a PluginError should be raised + And the plugin error message should contain "execution failed" + + # ----------------------------------------------------------------------- + # Guard: execute_plugin requires ACTIVATED state + # ----------------------------------------------------------------------- + + Scenario: execute_plugin raises PluginError if plugin is not activated + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I attempt to execute method on non-activated plugin "exec-plugin" + Then a PluginError should be raised + + Scenario: execute_plugin raises PluginError if plugin is in ERRORED state + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I activate the plugin "exec-plugin" + And I attempt to execute a failing method on plugin "exec-plugin" + And I attempt to execute method on non-activated plugin "exec-plugin" + Then a PluginError should be raised + + # ----------------------------------------------------------------------- + # Thread safety: EXECUTING state prevents concurrent deactivation + # ----------------------------------------------------------------------- + + Scenario: Cannot deactivate a plugin that is in EXECUTING state + Given a fresh PluginManager instance + And a PluginDescriptor for "exec-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend" + When I register the plugin descriptor + And I activate the plugin "exec-plugin" + And I manually set plugin "exec-plugin" state to "executing" + And I attempt to deactivate the plugin "exec-plugin" + Then a PluginError should be raised + + # ----------------------------------------------------------------------- + # PluginState enum includes EXECUTING + # ----------------------------------------------------------------------- + + Scenario: PluginState enum includes the EXECUTING value + Given the PluginState enum is available + Then it should have values "discovered", "activated", "executing", "deactivated", "errored" + + # ----------------------------------------------------------------------- + # execute_plugin raises PluginNotFoundError for unknown plugin + # ----------------------------------------------------------------------- + + Scenario: execute_plugin raises PluginNotFoundError for unknown plugin + Given a fresh PluginManager instance + And I attempt to execute method on unknown plugin "nonexistent-plugin" + Then a PluginNotFoundError should be raised diff --git a/features/steps/plugin_executing_state_steps.py b/features/steps/plugin_executing_state_steps.py new file mode 100644 index 000000000..b68066f72 --- /dev/null +++ b/features/steps/plugin_executing_state_steps.py @@ -0,0 +1,136 @@ +"""Behave step definitions for PluginState.EXECUTING lifecycle state. + +Covers the execute_plugin() method on PluginManager, which transitions +plugins through ACTIVATED -> EXECUTING -> ACTIVATED (success) or +ACTIVATED -> EXECUTING -> ERRORED (failure). + +Based on issue #5691. +""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +from behave import given, then, when # type: ignore[import-untyped] +from behave.runner import Context # type: ignore[import-untyped] + +from cleveragents.infrastructure.plugins.exceptions import ( + PluginError, + PluginNotFoundError, +) +from cleveragents.infrastructure.plugins.manager import PluginManager +from cleveragents.infrastructure.plugins.types import ( + PluginDescriptor, + PluginState, +) + + +# --------------------------------------------------------------------------- +# Execute plugin — happy path +# --------------------------------------------------------------------------- + + +@when('I execute method "search" on plugin "{name}" with valid args') +def step_execute_plugin_search(context: Context, name: str) -> None: + """Execute the search method on an InMemoryTextBackend plugin.""" + context.caught_exception = None + context.execution_result = None + try: + context.execution_result = context.manager.execute_plugin( + name, + "search", + "test query", + scope=frozenset(), + max_results=5, + ) + except PluginError as exc: + context.caught_exception = exc + + +@then("the execution result should be an empty list") +def step_execution_result_empty_list(context: Context) -> None: + assert context.execution_result == [], ( + f"Expected empty list, got {context.execution_result!r}" + ) + + +# --------------------------------------------------------------------------- +# Execute plugin — error path +# --------------------------------------------------------------------------- + + +@when('I attempt to execute a failing method on plugin "{name}"') +def step_attempt_execute_failing(context: Context, name: str) -> None: + """Execute a method that raises an exception on the plugin.""" + context.caught_exception = None + # Patch the instance's search method to raise a RuntimeError + instance = context.manager.get_plugin_instance(name) + if instance is not None: + with patch.object(instance, "search", side_effect=RuntimeError("boom")): + try: + context.manager.execute_plugin( + name, + "search", + "test query", + scope=frozenset(), + max_results=5, + ) + except PluginError as exc: + context.caught_exception = exc + else: + # Plugin not activated — try to execute anyway to get the error + try: + context.manager.execute_plugin(name, "search", "test", scope=frozenset()) + except PluginError as exc: + context.caught_exception = exc + + +# --------------------------------------------------------------------------- +# Execute plugin — guard: not activated +# --------------------------------------------------------------------------- + + +@when('I attempt to execute method on non-activated plugin "{name}"') +def step_attempt_execute_non_activated(context: Context, name: str) -> None: + """Attempt to execute a method on a plugin that is not in ACTIVATED state.""" + context.caught_exception = None + try: + context.manager.execute_plugin( + name, + "search", + "test query", + scope=frozenset(), + max_results=5, + ) + except PluginError as exc: + context.caught_exception = exc + + +@when('I attempt to execute method on unknown plugin "{name}"') +def step_attempt_execute_unknown(context: Context, name: str) -> None: + """Attempt to execute a method on a plugin that is not registered.""" + context.caught_exception = None + try: + context.manager.execute_plugin( + name, + "search", + "test query", + scope=frozenset(), + max_results=5, + ) + except (PluginError, PluginNotFoundError) as exc: + context.caught_exception = exc + + + +# --------------------------------------------------------------------------- +# Thread safety: EXECUTING state prevents deactivation +# --------------------------------------------------------------------------- + + +@when('I manually set plugin "{name}" state to "{state}"') +def step_manually_set_state(context: Context, name: str, state: str) -> None: + """Manually set a plugin's state for testing purposes.""" + descriptor = context.manager.get_plugin(name) + descriptor.state = PluginState(state) diff --git a/src/cleveragents/infrastructure/plugins/manager.py b/src/cleveragents/infrastructure/plugins/manager.py index 444a3b157..63dd36a37 100644 --- a/src/cleveragents/infrastructure/plugins/manager.py +++ b/src/cleveragents/infrastructure/plugins/manager.py @@ -53,6 +53,7 @@ class PluginManager: ) manager.register_plugin(descriptor) manager.activate_plugin("my-plugin") + manager.execute_plugin("my-plugin", "my_method", arg1, kwarg=val) info = manager.get_plugin("my-plugin") manager.deactivate_plugin("my-plugin") @@ -272,6 +273,70 @@ class PluginManager: msg = f"Failed to activate plugin '{name}': {exc}" raise PluginError(msg) from exc + # ------------------------------------------------------------------ + # Lifecycle: execute + # ------------------------------------------------------------------ + + def execute_plugin(self, name: str, method: str, *args: Any, **kwargs: Any) -> Any: + """Execute a method on an activated plugin. + + Transitions the plugin to ``EXECUTING`` state before calling the + method, then back to ``ACTIVATED`` on success, or ``ERRORED`` on + exception. The lock is released during execution so other threads + can query the registry while the plugin is running. + + Args: + name: Plugin name. + method: Name of the method to call on the plugin instance. + *args: Positional arguments forwarded to the method. + **kwargs: Keyword arguments forwarded to the method. + + Returns: + The return value of the plugin method. + + Raises: + PluginNotFoundError: If the plugin is not registered. + PluginError: If the plugin is not in ``ACTIVATED`` state, or + if the method call raises any exception. + """ + with self._lock: + descriptor = self.get_plugin(name) + if descriptor.state != PluginState.ACTIVATED: + msg = ( + f"Plugin '{name}' is not in ACTIVATED state " + f"(current: '{descriptor.state}')" + ) + raise PluginError(msg) + descriptor.state = PluginState.EXECUTING + self._logger.debug( + "plugin_manager.executing", + name=name, + method=method, + ) + + try: + instance = self._instances[name] + result = getattr(instance, method)(*args, **kwargs) + with self._lock: + descriptor.state = PluginState.ACTIVATED + self._logger.debug( + "plugin_manager.executed", + name=name, + method=method, + ) + return result + except Exception as exc: + with self._lock: + descriptor.state = PluginState.ERRORED + self._logger.warning( + "plugin_manager.execution_failed", + name=name, + method=method, + error=str(exc), + ) + msg = f"Plugin '{name}' execution failed: {exc}" + raise PluginError(msg) from exc + # ------------------------------------------------------------------ # Lifecycle: deactivate # ------------------------------------------------------------------ -- 2.52.0 From 4139addbf57468402dc098e4a212bb1c037a7d19 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Sun, 19 Apr 2026 00:55:52 +0000 Subject: [PATCH 2/6] fix(plugins): clean up unused imports in plugin_executing_state_steps.py Remove unused imports (Any, given, PluginManager, PluginDescriptor) and fix import ordering to satisfy ruff linting rules. --- features/steps/plugin_executing_state_steps.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/features/steps/plugin_executing_state_steps.py b/features/steps/plugin_executing_state_steps.py index b68066f72..d0242fc0e 100644 --- a/features/steps/plugin_executing_state_steps.py +++ b/features/steps/plugin_executing_state_steps.py @@ -9,22 +9,16 @@ Based on issue #5691. from __future__ import annotations -from typing import Any from unittest.mock import patch -from behave import given, then, when # type: ignore[import-untyped] +from behave import then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from cleveragents.infrastructure.plugins.exceptions import ( PluginError, PluginNotFoundError, ) -from cleveragents.infrastructure.plugins.manager import PluginManager -from cleveragents.infrastructure.plugins.types import ( - PluginDescriptor, - PluginState, -) - +from cleveragents.infrastructure.plugins.types import PluginState # --------------------------------------------------------------------------- # Execute plugin — happy path @@ -64,7 +58,6 @@ def step_execution_result_empty_list(context: Context) -> None: def step_attempt_execute_failing(context: Context, name: str) -> None: """Execute a method that raises an exception on the plugin.""" context.caught_exception = None - # Patch the instance's search method to raise a RuntimeError instance = context.manager.get_plugin_instance(name) if instance is not None: with patch.object(instance, "search", side_effect=RuntimeError("boom")): @@ -79,7 +72,6 @@ def step_attempt_execute_failing(context: Context, name: str) -> None: except PluginError as exc: context.caught_exception = exc else: - # Plugin not activated — try to execute anyway to get the error try: context.manager.execute_plugin(name, "search", "test", scope=frozenset()) except PluginError as exc: @@ -123,7 +115,6 @@ def step_attempt_execute_unknown(context: Context, name: str) -> None: context.caught_exception = exc - # --------------------------------------------------------------------------- # Thread safety: EXECUTING state prevents deactivation # --------------------------------------------------------------------------- -- 2.52.0 From 9b5a723f967aa08e668c588e1ef8180e6d76a4fb Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Fri, 24 Apr 2026 05:05:17 +0000 Subject: [PATCH 3/6] fix(plugins): add missing step definitions for EXECUTING state tests --- .../steps/plugin_executing_state_steps.py | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/features/steps/plugin_executing_state_steps.py b/features/steps/plugin_executing_state_steps.py index d0242fc0e..ff210236d 100644 --- a/features/steps/plugin_executing_state_steps.py +++ b/features/steps/plugin_executing_state_steps.py @@ -11,7 +11,7 @@ from __future__ import annotations from unittest.mock import patch -from behave import then, when # type: ignore[import-untyped] +from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from cleveragents.infrastructure.plugins.exceptions import ( @@ -125,3 +125,94 @@ def step_manually_set_state(context: Context, name: str, state: str) -> None: """Manually set a plugin's state for testing purposes.""" descriptor = context.manager.get_plugin(name) descriptor.state = PluginState(state) + + +# --------------------------------------------------------------------------- +# State assertions +# --------------------------------------------------------------------------- + + +@then('the plugin "{name}" state should be "{expected_state}"') +def step_plugin_state_should_be(context: Context, name: str, expected_state: str) -> None: + """Assert that a plugin is in the expected state.""" + descriptor = context.manager.get_plugin(name) + assert descriptor.state == PluginState(expected_state), ( + f"Expected plugin '{name}' to be in state '{expected_state}', " + f"but it is in state '{descriptor.state}'" + ) + + +@then("a PluginError should be raised") +def step_plugin_error_raised(context: Context) -> None: + """Assert that a PluginError was raised.""" + assert context.caught_exception is not None, ( + "Expected a PluginError to be raised, but no exception was caught" + ) + assert isinstance(context.caught_exception, PluginError), ( + f"Expected PluginError, but got {type(context.caught_exception).__name__}" + ) + + +@then("a PluginNotFoundError should be raised") +def step_plugin_not_found_error_raised(context: Context) -> None: + """Assert that a PluginNotFoundError was raised.""" + assert context.caught_exception is not None, ( + "Expected a PluginNotFoundError to be raised, but no exception was caught" + ) + assert isinstance(context.caught_exception, PluginNotFoundError), ( + f"Expected PluginNotFoundError, but got {type(context.caught_exception).__name__}" + ) + + +@then('the plugin error message should contain "{expected_text}"') +def step_plugin_error_message_contains(context: Context, expected_text: str) -> None: + """Assert that the plugin error message contains expected text.""" + assert context.caught_exception is not None, ( + "Expected an exception to be raised, but none was caught" + ) + error_message = str(context.caught_exception) + assert expected_text in error_message, ( + f"Expected error message to contain '{expected_text}', " + f"but got: {error_message}" + ) + + +# --------------------------------------------------------------------------- +# Deactivation guard +# --------------------------------------------------------------------------- + + +@when('I attempt to deactivate the plugin "{name}"') +def step_attempt_deactivate_plugin(context: Context, name: str) -> None: + """Attempt to deactivate a plugin.""" + context.caught_exception = None + try: + context.manager.deactivate_plugin(name) + except PluginError as exc: + context.caught_exception = exc + + +# --------------------------------------------------------------------------- +# PluginState enum verification +# --------------------------------------------------------------------------- + + +@given("the PluginState enum is available") +def step_plugin_state_enum_available(context: Context) -> None: + """Verify that the PluginState enum is available.""" + context.plugin_state_enum = PluginState + + +@then('it should have values "{values}"') +def step_enum_has_values(context: Context, values: str) -> None: + """Assert that the PluginState enum has the expected values.""" + expected_values = set(values.split('", "')) + # Clean up the first and last values (they have extra quotes) + expected_values = {v.strip('"') for v in expected_values} + + actual_values = {state.value for state in context.plugin_state_enum} + + assert actual_values == expected_values, ( + f"Expected PluginState enum to have values {expected_values}, " + f"but got {actual_values}" + ) -- 2.52.0 From 391c9f4fe75f05893b4d826830cd6fefaca6ded1 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Fri, 24 Apr 2026 05:11:58 +0000 Subject: [PATCH 4/6] fix(plugins): remove trailing whitespace in step definitions --- features/steps/plugin_executing_state_steps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/plugin_executing_state_steps.py b/features/steps/plugin_executing_state_steps.py index ff210236d..b9e451b88 100644 --- a/features/steps/plugin_executing_state_steps.py +++ b/features/steps/plugin_executing_state_steps.py @@ -209,9 +209,9 @@ def step_enum_has_values(context: Context, values: str) -> None: expected_values = set(values.split('", "')) # Clean up the first and last values (they have extra quotes) expected_values = {v.strip('"') for v in expected_values} - + actual_values = {state.value for state in context.plugin_state_enum} - + assert actual_values == expected_values, ( f"Expected PluginState enum to have values {expected_values}, " f"but got {actual_values}" -- 2.52.0 From 09d51237f065e6f3d2cc4c86a4b5e4bd443efced Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Fri, 24 Apr 2026 05:32:53 +0000 Subject: [PATCH 5/6] fix(plugins): resolve step definition conflicts and update feature file --- features/steps/plugin_executing_state_steps.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/features/steps/plugin_executing_state_steps.py b/features/steps/plugin_executing_state_steps.py index b9e451b88..615d5e80b 100644 --- a/features/steps/plugin_executing_state_steps.py +++ b/features/steps/plugin_executing_state_steps.py @@ -128,20 +128,10 @@ def step_manually_set_state(context: Context, name: str, state: str) -> None: # --------------------------------------------------------------------------- -# State assertions +# Exception assertions # --------------------------------------------------------------------------- -@then('the plugin "{name}" state should be "{expected_state}"') -def step_plugin_state_should_be(context: Context, name: str, expected_state: str) -> None: - """Assert that a plugin is in the expected state.""" - descriptor = context.manager.get_plugin(name) - assert descriptor.state == PluginState(expected_state), ( - f"Expected plugin '{name}' to be in state '{expected_state}', " - f"but it is in state '{descriptor.state}'" - ) - - @then("a PluginError should be raised") def step_plugin_error_raised(context: Context) -> None: """Assert that a PluginError was raised.""" -- 2.52.0 From 552dcb15b02471ca036556f2cf5324c69d7a7388 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Thu, 4 Jun 2026 19:29:15 -0400 Subject: [PATCH 6/6] fix(plugins): remove duplicate step definitions and fix keyword context in feature AmbiguousStep error caused all 8 features to error: plugin_executing_state_steps.py re-defined 6 steps already present in plugin_architecture_steps.py: - @then("a PluginError should be raised") - @then("a PluginNotFoundError should be raised") - @then('the plugin error message should contain "{text}"') - @when('I attempt to deactivate the plugin "{name}"') - @given("the PluginState enum is available") - @then('it should have values ...') Remove all duplicates, keeping only the 6 new steps unique to this feature. Also fix the last scenario where `And I attempt to execute method on unknown plugin` followed a `Given` step (so behave treated it as `given` context, not matching the `@when` definition). Changed `And` to `When`. ISSUES CLOSED: #5691 --- features/plugin_executing_state.feature | 2 +- .../steps/plugin_executing_state_steps.py | 88 +------------------ 2 files changed, 2 insertions(+), 88 deletions(-) diff --git a/features/plugin_executing_state.feature b/features/plugin_executing_state.feature index 14575785c..f398a10b4 100644 --- a/features/plugin_executing_state.feature +++ b/features/plugin_executing_state.feature @@ -95,5 +95,5 @@ Feature: PluginState.EXECUTING lifecycle state Scenario: execute_plugin raises PluginNotFoundError for unknown plugin Given a fresh PluginManager instance - And I attempt to execute method on unknown plugin "nonexistent-plugin" + When I attempt to execute method on unknown plugin "nonexistent-plugin" Then a PluginNotFoundError should be raised diff --git a/features/steps/plugin_executing_state_steps.py b/features/steps/plugin_executing_state_steps.py index 615d5e80b..e1af73ce1 100644 --- a/features/steps/plugin_executing_state_steps.py +++ b/features/steps/plugin_executing_state_steps.py @@ -11,7 +11,7 @@ from __future__ import annotations from unittest.mock import patch -from behave import given, then, when # type: ignore[import-untyped] +from behave import then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from cleveragents.infrastructure.plugins.exceptions import ( @@ -27,7 +27,6 @@ from cleveragents.infrastructure.plugins.types import PluginState @when('I execute method "search" on plugin "{name}" with valid args') def step_execute_plugin_search(context: Context, name: str) -> None: - """Execute the search method on an InMemoryTextBackend plugin.""" context.caught_exception = None context.execution_result = None try: @@ -56,7 +55,6 @@ def step_execution_result_empty_list(context: Context) -> None: @when('I attempt to execute a failing method on plugin "{name}"') def step_attempt_execute_failing(context: Context, name: str) -> None: - """Execute a method that raises an exception on the plugin.""" context.caught_exception = None instance = context.manager.get_plugin_instance(name) if instance is not None: @@ -85,7 +83,6 @@ def step_attempt_execute_failing(context: Context, name: str) -> None: @when('I attempt to execute method on non-activated plugin "{name}"') def step_attempt_execute_non_activated(context: Context, name: str) -> None: - """Attempt to execute a method on a plugin that is not in ACTIVATED state.""" context.caught_exception = None try: context.manager.execute_plugin( @@ -101,7 +98,6 @@ def step_attempt_execute_non_activated(context: Context, name: str) -> None: @when('I attempt to execute method on unknown plugin "{name}"') def step_attempt_execute_unknown(context: Context, name: str) -> None: - """Attempt to execute a method on a plugin that is not registered.""" context.caught_exception = None try: context.manager.execute_plugin( @@ -122,87 +118,5 @@ def step_attempt_execute_unknown(context: Context, name: str) -> None: @when('I manually set plugin "{name}" state to "{state}"') def step_manually_set_state(context: Context, name: str, state: str) -> None: - """Manually set a plugin's state for testing purposes.""" descriptor = context.manager.get_plugin(name) descriptor.state = PluginState(state) - - -# --------------------------------------------------------------------------- -# Exception assertions -# --------------------------------------------------------------------------- - - -@then("a PluginError should be raised") -def step_plugin_error_raised(context: Context) -> None: - """Assert that a PluginError was raised.""" - assert context.caught_exception is not None, ( - "Expected a PluginError to be raised, but no exception was caught" - ) - assert isinstance(context.caught_exception, PluginError), ( - f"Expected PluginError, but got {type(context.caught_exception).__name__}" - ) - - -@then("a PluginNotFoundError should be raised") -def step_plugin_not_found_error_raised(context: Context) -> None: - """Assert that a PluginNotFoundError was raised.""" - assert context.caught_exception is not None, ( - "Expected a PluginNotFoundError to be raised, but no exception was caught" - ) - assert isinstance(context.caught_exception, PluginNotFoundError), ( - f"Expected PluginNotFoundError, but got {type(context.caught_exception).__name__}" - ) - - -@then('the plugin error message should contain "{expected_text}"') -def step_plugin_error_message_contains(context: Context, expected_text: str) -> None: - """Assert that the plugin error message contains expected text.""" - assert context.caught_exception is not None, ( - "Expected an exception to be raised, but none was caught" - ) - error_message = str(context.caught_exception) - assert expected_text in error_message, ( - f"Expected error message to contain '{expected_text}', " - f"but got: {error_message}" - ) - - -# --------------------------------------------------------------------------- -# Deactivation guard -# --------------------------------------------------------------------------- - - -@when('I attempt to deactivate the plugin "{name}"') -def step_attempt_deactivate_plugin(context: Context, name: str) -> None: - """Attempt to deactivate a plugin.""" - context.caught_exception = None - try: - context.manager.deactivate_plugin(name) - except PluginError as exc: - context.caught_exception = exc - - -# --------------------------------------------------------------------------- -# PluginState enum verification -# --------------------------------------------------------------------------- - - -@given("the PluginState enum is available") -def step_plugin_state_enum_available(context: Context) -> None: - """Verify that the PluginState enum is available.""" - context.plugin_state_enum = PluginState - - -@then('it should have values "{values}"') -def step_enum_has_values(context: Context, values: str) -> None: - """Assert that the PluginState enum has the expected values.""" - expected_values = set(values.split('", "')) - # Clean up the first and last values (they have extra quotes) - expected_values = {v.strip('"') for v in expected_values} - - actual_values = {state.value for state in context.plugin_state_enum} - - assert actual_values == expected_values, ( - f"Expected PluginState enum to have values {expected_values}, " - f"but got {actual_values}" - ) -- 2.52.0