diff --git a/features/environment.py b/features/environment.py index 4e9d411ae..b1d23a620 100644 --- a/features/environment.py +++ b/features/environment.py @@ -713,6 +713,19 @@ def after_scenario(context, scenario): except ImportError: pass + # Reset session CLI module-level service singleton so that no stale + # service instance leaks into the next scenario. Without this reset, + # a scenario that sets _service to a real container-constructed instance + # would leave it cached; a subsequent scenario that patches _service + # via unittest.mock.patch would restore to the stale real instance on + # cleanup, causing the next scenario to hit the real container and fail. + try: + from cleveragents.cli.commands.session import _reset_session_service + + _reset_session_service() + except ImportError: + pass + # Clean up service instances for attr in ["context_service", "plan_service", "project_service"]: if hasattr(context, attr): diff --git a/features/steps/session_cli_coverage_boost_steps.py b/features/steps/session_cli_coverage_boost_steps.py index 306fc6359..f4cf7136a 100644 --- a/features/steps/session_cli_coverage_boost_steps.py +++ b/features/steps/session_cli_coverage_boost_steps.py @@ -101,7 +101,20 @@ def _mock_service() -> MagicMock: def _patch_service(context, svc): - patcher = patch("cleveragents.cli.commands.session._service", svc) + """Patch _get_session_service to return *svc* deterministically. + + Patching the function directly (rather than the module-level ``_service`` + attribute) prevents a race condition in parallel Behave workers: if a + concurrent worker's cleanup resets ``_service`` to ``None`` while this + scenario's CLI command is executing, ``_get_session_service()`` would fall + through to the real DI container and raise, producing a spurious exit + code 1. Patching the function itself short-circuits the cache check + entirely and is immune to cross-worker ``_service`` mutations. + """ + patcher = patch( + "cleveragents.cli.commands.session._get_session_service", + return_value=svc, + ) patcher.start() context._scvbst_cleanups.append(patcher.stop) diff --git a/features/steps/session_cli_uncovered_branches_steps.py b/features/steps/session_cli_uncovered_branches_steps.py index cc0900eb5..1f6f54203 100644 --- a/features/steps/session_cli_uncovered_branches_steps.py +++ b/features/steps/session_cli_uncovered_branches_steps.py @@ -72,12 +72,24 @@ def _mock_service() -> MagicMock: return svc -# ---- helpers to patch the module-level _service --------------------------- +# ---- helpers to patch the module-level service accessor ------------------- def _patch_service(context, svc): - """Patch session._service and register cleanup.""" - patcher = patch("cleveragents.cli.commands.session._service", svc) + """Patch _get_session_service to return *svc* deterministically. + + Patching the function directly (rather than the module-level ``_service`` + attribute) prevents a race condition in parallel Behave workers: if a + concurrent worker's cleanup resets ``_service`` to ``None`` while this + scenario's CLI command is executing, ``_get_session_service()`` would fall + through to the real DI container and raise, producing a spurious exit + code 1. Patching the function itself short-circuits the cache check + entirely and is immune to cross-worker ``_service`` mutations. + """ + patcher = patch( + "cleveragents.cli.commands.session._get_session_service", + return_value=svc, + ) patcher.start() context._cleanup_handlers.append(patcher.stop)