forked from cleveragents/cleveragents-core
fix(test): isolate parallel behave subprocesses with per-scenario temp databases
Each behave-parallel worker previously resolved to the same file-based SQLite database (cleveragents.db or .cleveragents/db.sqlite) because before_scenario removed the CLEVERAGENTS_DATABASE_URL env var and the fallback paths are shared across processes. Under parallel execution this caused intermittent 'database is locked' and duplicate-project errors—most visibly in the coverage_report nox session. Replace the env-var removal in before_scenario with unique temp database paths via tempfile.mktemp(), giving every scenario its own isolated database file. Temp files are cleaned up in after_scenario. Also fix cli_streaming_steps.py where a Background + duplicate Given sequence triggered a duplicate project name collision, and update coverage_boost_steps.py so Settings-default assertions explicitly clear the env vars before testing pydantic defaults.
This commit is contained in:
+29
-8
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user