fix(concurrency): make ensure_default_mock_actor atomic to prevent TOCTOU race

Merge the two separate database transactions in ensure_default_mock_actor()
into a single atomic transaction to prevent Time-of-Check-Time-of-Use (TOCTOU)
race conditions in concurrent environments (e.g., parallel test workers).

The original implementation had a gap between checking for an existing default
actor and creating a new mock actor. In concurrent scenarios, multiple threads
could both observe no default and attempt to create one, leading to potential
constraint violations or inconsistent state.

The fix consolidates both operations into a single transaction, ensuring that
the check and creation are atomic and safe for concurrent callers.

Added BDD test scenarios to verify idempotency and concurrent safety:
- Idempotency: calling ensure_default_mock_actor multiple times produces same result
- Existing default safety: respects existing default actors
- Single actor creation: ensures only one mock actor is created

Closes #8448
This commit is contained in:
2026-04-18 21:16:24 +00:00
committed by Forgejo
parent 790eb6f001
commit 4e37da2471
3 changed files with 74 additions and 18 deletions
+27
View File
@@ -296,3 +296,30 @@ def after_scenario(context: Context, scenario) -> None:
if hasattr(context, "_cleanup_handlers"):
for handler in context._cleanup_handlers:
handler()
@when('I ensure the default mock actor with env value "{value}" again')
def step_ensure_default_mock_actor_again(context: Context, value: str) -> None:
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = value
context.result_actor = context.actor_service.ensure_default_mock_actor()
@then('only one "{name}" actor should exist')
def step_only_one_actor_exists(context: Context, name: str) -> None:
actors = context.actor_repo.list_all()
count = sum(1 for actor in actors if actor.name == name)
assert count == 1, f"Expected 1 actor named {name}, but found {count}"
@then("only one default actor should exist")
def step_only_one_default_actor(context: Context) -> None:
actors = context.actor_repo.list_all()
default_count = sum(1 for actor in actors if actor.is_default)
assert default_count == 1, f"Expected 1 default actor, but found {default_count}"
@then("exactly one actor should exist")
def step_exactly_one_actor(context: Context) -> None:
actors = context.actor_repo.list_all()
count = len(actors)
assert count == 1, f"Expected 1 actor, but found {count}"