fix/database-parallelism #410

Merged
brent.edwards merged 4 commits from fix/database-parallelism into master 2026-02-24 19:06:37 +00:00
3 changed files with 46 additions and 8 deletions
+29 -8
View File
@@ -4,6 +4,7 @@ import contextlib
import os
import shutil
import sys
import tempfile
from pathlib import Path
LANGSMITH_ENV_VARS = [
@@ -32,10 +33,16 @@ def before_all(context):
# Ensure tests never block on migration prompts or real providers
os.environ.setdefault("CLEVERAGENTS_AUTO_APPLY_MIGRATIONS", "true")
os.environ.setdefault("CLEVERAGENTS_TESTING_USE_MOCK_AI", "true")
os.environ.setdefault("CLEVERAGENTS_DATABASE_URL", "sqlite:///cleveragents.db")
os.environ.setdefault(
"CLEVERAGENTS_TEST_DATABASE_URL", "sqlite:///cleveragents_test.db"
)
# Use per-process unique database paths so parallel test subprocesses
# (behave-parallel) never contend on the same SQLite file.
if "CLEVERAGENTS_DATABASE_URL" not in os.environ:
os.environ["CLEVERAGENTS_DATABASE_URL"] = (
f"sqlite:///{tempfile.mktemp(suffix='.db', prefix='cleveragents_')}"
)
if "CLEVERAGENTS_TEST_DATABASE_URL" not in os.environ:
os.environ["CLEVERAGENTS_TEST_DATABASE_URL"] = (
f"sqlite:///{tempfile.mktemp(suffix='.db', prefix='cleveragents_test_')}"
)
os.environ.setdefault("BEHAVE_TESTING", "true")
# Set up mock AI provider for all tests
@@ -92,10 +99,17 @@ def before_scenario(context, scenario):
if env_var in os.environ:
del os.environ[env_var]
# Ensure each scenario starts with a fresh database URL so tests
# don't share persisted project state across runs
for env_var in ["CLEVERAGENTS_DATABASE_URL", "CLEVERAGENTS_TEST_DATABASE_URL"]:
os.environ.pop(env_var, None)
# Give each scenario a unique database file so scenarios cannot share
# persisted state AND parallel subprocesses never collide on the same
# SQLite file. Store the paths for cleanup in after_scenario.
context._scenario_db_paths = []
for env_var, prefix in (
("CLEVERAGENTS_DATABASE_URL", "cleveragents_"),
("CLEVERAGENTS_TEST_DATABASE_URL", "cleveragents_test_"),
):
db_path = tempfile.mktemp(suffix=".db", prefix=prefix)
os.environ[env_var] = f"sqlite:///{db_path}"
context._scenario_db_paths.append(db_path)
# Re-apply mock AI provider after container reset
try:
@@ -205,3 +219,10 @@ def after_scenario(context, scenario):
MEMORY_ENGINES.clear()
except ImportError:
pass
# Remove per-scenario temp database files (and associated journal/WAL
# files) now that all engines have been disposed.
for db_path in getattr(context, "_scenario_db_paths", []):
for suffix in ("", "-journal", "-wal", "-shm"):
with contextlib.suppress(OSError):
os.unlink(db_path + suffix)
+7
View File
@@ -31,6 +31,13 @@ def step_create_temp_dir(context):
context.original_dir = os.getcwd()
os.chdir(context.temp_dir)
# Give this invocation a unique database so repeated calls within the
# same scenario (e.g. Background + explicit Given) never collide.
db_path = tempfile.mktemp(suffix=".db", prefix="cleveragents_")
os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}"
context._scenario_db_paths = getattr(context, "_scenario_db_paths", [])
context._scenario_db_paths.append(db_path)
# Reset container for clean state
from cleveragents.application.container import reset_container
+10
View File
@@ -14,6 +14,10 @@ def step_create_settings(context):
# Clear any existing singleton
if hasattr(Settings, "_instance"):
Settings._instance = None
# Remove database URL env vars so we test the actual pydantic defaults,
# not the per-scenario temp paths injected by environment.py.
for key in ("CLEVERAGENTS_DATABASE_URL", "CLEVERAGENTS_TEST_DATABASE_URL"):
os.environ.pop(key, None)
context.settings = Settings()
@@ -44,6 +48,12 @@ def step_verify_production_status(context):
@when("I get the database URL from Settings")
def step_get_database_url(context):
"""Get database URL."""
# Clear singleton and database URL env vars so we test the actual
# pydantic defaults, not per-scenario temp paths from environment.py.
if hasattr(Settings, "_instance"):
Settings._instance = None
for key in ("CLEVERAGENTS_DATABASE_URL", "CLEVERAGENTS_TEST_DATABASE_URL"):
os.environ.pop(key, None)
settings = Settings()
context.db_url = settings.get_database_url()
context.test_db_url = settings.get_database_url(test=True)