diff --git a/features/environment.py b/features/environment.py index bd3af84a6..79f1da027 100644 --- a/features/environment.py +++ b/features/environment.py @@ -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) diff --git a/features/steps/cli_streaming_steps.py b/features/steps/cli_streaming_steps.py index f30c69e56..3cfc0611c 100644 --- a/features/steps/cli_streaming_steps.py +++ b/features/steps/cli_streaming_steps.py @@ -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 diff --git a/features/steps/coverage_boost_steps.py b/features/steps/coverage_boost_steps.py index 1d909d29d..fa0c14ddc 100644 --- a/features/steps/coverage_boost_steps.py +++ b/features/steps/coverage_boost_steps.py @@ -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)