0d4a47f3c0
CI / lint (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 44s
CI / security (pull_request) Successful in 52s
CI / build (pull_request) Successful in 24s
CI / helm (pull_request) Successful in 22s
CI / unit_tests (pull_request) Successful in 6m37s
CI / e2e_tests (pull_request) Successful in 17m48s
CI / integration_tests (pull_request) Successful in 22m31s
CI / docker (pull_request) Successful in 1m24s
CI / coverage (pull_request) Successful in 10m26s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m45s
- Fix Settings.database_url default from relative 'sqlite:///cleveragents.db' to absolute 'sqlite:////home/<user>/.cleveragents/cleveragents.db' using Path.home() / '.cleveragents' / 'cleveragents.db' - Fix test_database_url default similarly to use ~/.cleveragents/cleveragents_test.db - Fix alembic/env.py fallback default from Path.cwd() / '.cleveragents' / 'db.sqlite' to Path.home() / '.cleveragents' / 'cleveragents.db' (matching Settings) - Both Settings and alembic/env.py now resolve to the same absolute path - CLEVERAGENTS_DATABASE_URL env var override still works correctly in both - Update coverage_boost_steps.py assertions to match new correct defaults - Add 3 new Behave scenarios covering correct database_url default and env var override ISSUES CLOSED: #2871
703 lines
26 KiB
Python
703 lines
26 KiB
Python
"""Step definitions for settings feature tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.config.settings import Settings
|
|
|
|
|
|
@given('the environment variable "{key}" is set to "{value}"')
|
|
@when('the environment variable "{key}" is set to "{value}"')
|
|
def step_set_env_var(context, key, value):
|
|
"""Set an environment variable."""
|
|
os.environ[key] = value
|
|
if not hasattr(context, "env_vars_to_clean"):
|
|
context.env_vars_to_clean = []
|
|
context.env_vars_to_clean.append(key)
|
|
|
|
|
|
@given("no environment variables are set")
|
|
def step_clear_env_vars(context):
|
|
"""Clear all CleverAgents-related environment variables."""
|
|
keys_to_remove = [
|
|
"CLEVERAGENTS_LOG_LEVEL",
|
|
"CLEVERAGENTS_LOG_DIR",
|
|
"CLEVERAGENTS_DATA_DIR",
|
|
"CLEVERAGENTS_ENV",
|
|
"CLEVERAGENTS_DEFAULT_PROVIDER",
|
|
"CLEVERAGENTS_DEFAULT_MODEL",
|
|
"CLEVERAGENTS_OPENROUTER_ORGANIZATION",
|
|
"DATABASE_URL",
|
|
"OPENAI_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
"OPENROUTER_ORGANIZATION",
|
|
"ANTHROPIC_API_KEY",
|
|
"GOOGLE_API_KEY",
|
|
"GOOGLE_GENAI_API_KEY",
|
|
"GEMINI_API_KEY",
|
|
"AZURE_OPENAI_API_KEY",
|
|
"AZURE_API_KEY",
|
|
"AZURE_OPENAI_ENDPOINT",
|
|
"AZURE_OPENAI_DEPLOYMENT",
|
|
"AZURE_OPENAI_API_VERSION",
|
|
"DEEPSEEK_API_KEY",
|
|
"COHERE_API_KEY",
|
|
"PERPLEXITY_API_KEY",
|
|
"GROQ_API_KEY",
|
|
"TOGETHER_API_KEY",
|
|
"HF_TOKEN",
|
|
]
|
|
for key in keys_to_remove:
|
|
os.environ.pop(key, None)
|
|
|
|
|
|
@when("I load the settings")
|
|
def step_load_settings(context):
|
|
"""Load settings from environment."""
|
|
# Clear the singleton to force reload
|
|
Settings._instance = None
|
|
context.settings = Settings()
|
|
|
|
|
|
@when("I load the settings with defaults")
|
|
def step_load_settings_defaults(context):
|
|
"""Load settings with defaults."""
|
|
# Clear the singleton to force reload
|
|
Settings._instance = None
|
|
context.settings = Settings()
|
|
|
|
|
|
@when('I construct settings with default provider "{provider}"')
|
|
def step_construct_settings_default_provider(context, provider):
|
|
"""Construct a settings instance with an explicit default provider value."""
|
|
Settings._instance = None
|
|
context.settings = Settings.model_construct(default_provider=provider)
|
|
|
|
|
|
@when('I construct settings with default model "{model}"')
|
|
def step_construct_settings_default_model(context, model):
|
|
"""Construct a settings instance with an explicit default model value."""
|
|
Settings._instance = None
|
|
context.settings = Settings.model_construct(default_model=model)
|
|
|
|
|
|
@then('the log level should be "{expected}"')
|
|
def step_check_log_level(context, expected):
|
|
"""Check the log level."""
|
|
assert context.settings.log_level == expected
|
|
|
|
|
|
@then('the log directory should be "{expected}"')
|
|
def step_check_log_dir(context, expected):
|
|
"""Check the log directory."""
|
|
assert str(context.settings.log_dir) == expected
|
|
|
|
|
|
@then('the log directory should contain "{substring}"')
|
|
def step_check_log_dir_contains(context, substring):
|
|
"""Check the log directory contains a substring."""
|
|
assert substring in str(context.settings.log_dir)
|
|
|
|
|
|
@then('the data directory should be "{expected}"')
|
|
def step_check_data_dir(context, expected):
|
|
"""Check the data directory."""
|
|
assert str(context.settings.data_dir) == expected
|
|
|
|
|
|
@then('the data directory should contain "{substring}"')
|
|
def step_check_data_dir_contains(context, substring):
|
|
"""Check the data directory contains a substring."""
|
|
assert substring in str(context.settings.data_dir)
|
|
|
|
|
|
@then('the environment should be "{expected}"')
|
|
def step_check_environment(context, expected):
|
|
"""Check the environment."""
|
|
assert context.settings.env == expected
|
|
|
|
|
|
@when('I set the environment alias to "{value}"')
|
|
def step_set_environment_alias(context, value):
|
|
"""Update the environment alias property."""
|
|
assert hasattr(context, "settings"), (
|
|
"Settings must be loaded before updating the environment alias."
|
|
)
|
|
context.settings.environment = value
|
|
|
|
|
|
@then('the environment alias should be "{expected}"')
|
|
def step_check_environment_alias(context, expected):
|
|
"""Validate the environment alias getter."""
|
|
assert context.settings.environment == expected
|
|
|
|
|
|
@then('the database URL should be "{expected}"')
|
|
def step_check_database_url(context, expected):
|
|
"""Check the database URL."""
|
|
assert context.settings.database_url == expected
|
|
|
|
|
|
@then("the database URL should be a SQLite database")
|
|
def step_check_sqlite_database(context):
|
|
"""Check that the database URL is SQLite."""
|
|
assert context.settings.database_url.startswith("sqlite://")
|
|
|
|
|
|
@then("Anthropic should be configured")
|
|
def step_check_anthropic_configured(context):
|
|
"""Check that Anthropic is configured."""
|
|
assert context.settings.anthropic_api_key is not None
|
|
|
|
|
|
@then("no providers should be configured")
|
|
def step_check_no_providers(context):
|
|
"""Check that no providers are configured."""
|
|
assert not context.settings.has_provider_configured()
|
|
|
|
|
|
@given('the data directory is "{directory}"')
|
|
def step_set_data_directory(context, directory):
|
|
"""Set the data directory for testing."""
|
|
context.test_data_dir = directory
|
|
|
|
|
|
@given('the storage base path is "{directory}"')
|
|
def step_set_storage_base_path(context, directory):
|
|
"""Record the storage base directory used for tests."""
|
|
context.storage_base_dir = directory
|
|
|
|
|
|
@when('I compute the storage path for "{storage_type}"')
|
|
def step_compute_storage_path(context, storage_type):
|
|
"""Compute the storage path from the configured base."""
|
|
base_dir = getattr(context, "storage_base_dir", None)
|
|
assert base_dir is not None, "Storage base path must be set before computing it."
|
|
settings = Settings.model_construct(storage_base_path=Path(base_dir))
|
|
context.storage_path = settings.get_storage_base_path(storage_type)
|
|
|
|
|
|
@when("I compute the default storage path")
|
|
def step_compute_default_storage_path(context):
|
|
"""Compute the storage path when no type is supplied."""
|
|
base_dir = getattr(context, "storage_base_dir", None)
|
|
assert base_dir is not None, "Storage base path must be set before computing it."
|
|
settings = Settings.model_construct(storage_base_path=Path(base_dir))
|
|
context.storage_path = settings.get_storage_base_path()
|
|
|
|
|
|
@when('I get the storage base path for "{storage_type}"')
|
|
def step_get_storage_path(context, storage_type):
|
|
"""Get the storage base path."""
|
|
settings = Settings(data_dir=Path(context.test_data_dir))
|
|
context.storage_path = settings.get_storage_base_path(storage_type)
|
|
|
|
|
|
@then('the storage path should be "{expected}"')
|
|
def step_check_storage_path(context, expected):
|
|
"""Check the storage path."""
|
|
assert str(context.storage_path) == expected
|
|
|
|
|
|
@given('the vector store path override is "{path_value}"')
|
|
def step_set_vector_store_override(context, path_value):
|
|
"""Record the vector store path override for later resolution."""
|
|
context.vector_store_override = path_value
|
|
|
|
|
|
@when("I resolve the vector store path")
|
|
def step_resolve_vector_store_path(context):
|
|
"""Resolve the configured vector store path to an absolute location."""
|
|
override = getattr(context, "vector_store_override", None)
|
|
assert override is not None, "Vector store path override must be provided first."
|
|
Settings._instance = None
|
|
settings = Settings.model_construct(vector_store_path=Path(override))
|
|
context.resolved_vector_store_path = settings.resolve_vector_store_path()
|
|
|
|
|
|
@then(
|
|
"the resolved vector store path should equal the override under the current working directory"
|
|
)
|
|
def step_verify_resolved_vector_store_path(context):
|
|
"""Ensure the vector store path is resolved relative to the current working directory."""
|
|
override = getattr(context, "vector_store_override", None)
|
|
assert override is not None, "Vector store path override must be provided first."
|
|
expected = (Path.cwd() / Path(override)).resolve()
|
|
assert context.resolved_vector_store_path == expected
|
|
|
|
|
|
@given('the environment is set to "{env}"')
|
|
def step_set_environment(context, env):
|
|
"""Set the environment."""
|
|
os.environ["CLEVERAGENTS_ENV"] = env
|
|
if not hasattr(context, "env_vars_to_clean"):
|
|
context.env_vars_to_clean = []
|
|
context.env_vars_to_clean.append("CLEVERAGENTS_ENV")
|
|
|
|
|
|
@when("I check if running in production")
|
|
def step_check_is_production(context):
|
|
"""Check if running in production."""
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
context.is_production = settings.is_production()
|
|
|
|
|
|
@when(
|
|
'I evaluate production mode with env "{env_value}", debug "{debug_flag}", and reload "{reload_flag}"'
|
|
)
|
|
def step_evaluate_production_mode_flags(context, env_value, debug_flag, reload_flag):
|
|
"""Evaluate production helpers for the supplied flags."""
|
|
Settings._instance = None
|
|
settings = Settings.model_construct(
|
|
env=env_value,
|
|
debug_enabled=debug_flag.lower() == "true",
|
|
server_reload=reload_flag.lower() == "true",
|
|
)
|
|
context.is_production = settings.is_production()
|
|
context.is_production_mode = settings.is_production_mode
|
|
|
|
|
|
@then("is_production should be {expected}")
|
|
def step_verify_is_production(context, expected):
|
|
"""Verify the is_production value."""
|
|
expected_bool = expected == "True"
|
|
assert context.is_production == expected_bool
|
|
|
|
|
|
@then("is_production_mode should be {expected}")
|
|
def step_verify_is_production_mode(context, expected):
|
|
"""Verify the is_production_mode alias."""
|
|
expected_bool = expected == "True"
|
|
assert context.is_production_mode == expected_bool
|
|
|
|
|
|
@given('the {provider} API key is set to "{key}"')
|
|
def step_set_provider_key(context, provider, key):
|
|
"""Set a provider API key."""
|
|
env_key = f"{provider.upper().replace(' ', '_')}_API_KEY"
|
|
os.environ[env_key] = key
|
|
if not hasattr(context, "env_vars_to_clean"):
|
|
context.env_vars_to_clean = []
|
|
context.env_vars_to_clean.append(env_key)
|
|
|
|
|
|
@when('I instantiate settings with explicit openai API key "{value}"')
|
|
def step_instantiate_settings_with_openai(context, value):
|
|
"""Instantiate settings with a constructor-specified OpenAI key."""
|
|
Settings._instance = None
|
|
context.settings = Settings.model_construct(openai_api_key=value)
|
|
|
|
|
|
@when('I check if "{provider}" provider is configured')
|
|
def step_check_provider_configured(context, provider):
|
|
"""Check if a provider is configured."""
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
context.provider_configured = settings.has_provider_configured(
|
|
provider.lower().replace(" ", "")
|
|
)
|
|
|
|
|
|
@when('I check if "{provider}" provider is configured using a constructed instance')
|
|
def step_check_provider_configured_constructed(context, provider):
|
|
"""Check provider configuration using a lightweight constructed settings instance."""
|
|
Settings._instance = None
|
|
settings = Settings.model_construct()
|
|
context.provider_configured = settings.has_provider_configured(
|
|
provider.lower().replace(" ", "")
|
|
)
|
|
|
|
|
|
@when('I clear the explicit "{provider}" key and recheck configuration')
|
|
def step_clear_explicit_key_and_recheck(context, provider):
|
|
"""Clear the in-memory provider key and re-evaluate configuration."""
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
attr = provider.lower().replace(" ", "_") + "_api_key"
|
|
setattr(settings, attr, None)
|
|
context.provider_configured = settings.has_provider_configured(provider)
|
|
|
|
|
|
@then('has_provider_configured should be {expected} for "{provider}"')
|
|
def step_verify_provider_configured(context, expected, provider):
|
|
"""Verify provider configuration status."""
|
|
expected_bool = expected == "True"
|
|
assert context.provider_configured == expected_bool
|
|
|
|
|
|
@then("has_provider_configured should be {expected}")
|
|
def step_verify_any_provider_configured(context, expected):
|
|
"""Verify any provider configuration status."""
|
|
expected_bool = expected == "True"
|
|
assert context.provider_configured == expected_bool
|
|
|
|
|
|
@given("no API keys are set")
|
|
def step_clear_api_keys(context):
|
|
"""Clear all API keys."""
|
|
api_keys = [
|
|
"OPENAI_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
"ANTHROPIC_API_KEY",
|
|
"GOOGLE_API_KEY",
|
|
"GOOGLE_GENAI_API_KEY",
|
|
"GEMINI_API_KEY",
|
|
"AZURE_OPENAI_API_KEY",
|
|
"AZURE_API_KEY",
|
|
"DEEPSEEK_API_KEY",
|
|
"COHERE_API_KEY",
|
|
"PERPLEXITY_API_KEY",
|
|
"GROQ_API_KEY",
|
|
"TOGETHER_API_KEY",
|
|
"HF_TOKEN",
|
|
]
|
|
for key in api_keys:
|
|
os.environ.pop(key, None)
|
|
|
|
|
|
@given('a temporary provider "{provider}" uses env "{env_var}"')
|
|
def step_define_temporary_provider(context, provider, env_var):
|
|
"""Temporarily register a provider mapping for provider lookup tests."""
|
|
normalized = provider.lower().replace(" ", "_")
|
|
if not normalized.endswith("_api_key"):
|
|
normalized = f"{normalized}_api_key"
|
|
|
|
original_map = Settings._PROVIDER_ENV_MAP
|
|
original_override = Settings._apply_external_env_overrides
|
|
|
|
Settings._PROVIDER_ENV_MAP = dict(Settings._PROVIDER_ENV_MAP)
|
|
Settings._PROVIDER_ENV_MAP[normalized] = (env_var,)
|
|
Settings._apply_external_env_overrides = lambda self: None
|
|
|
|
if not hasattr(context, "_cleanup_handlers"):
|
|
context._cleanup_handlers = []
|
|
|
|
def cleanup():
|
|
Settings._PROVIDER_ENV_MAP = original_map
|
|
Settings._apply_external_env_overrides = original_override
|
|
|
|
context._cleanup_handlers.append(cleanup)
|
|
|
|
|
|
@when("I check if any provider is configured")
|
|
def step_check_any_provider(context):
|
|
"""Check if any provider is configured."""
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
context.provider_configured = settings.has_provider_configured()
|
|
|
|
|
|
@given('the database URL is set to "{url}"')
|
|
def step_set_database_url(context, url):
|
|
"""Set the database URL."""
|
|
os.environ["DATABASE_URL"] = url
|
|
if not hasattr(context, "env_vars_to_clean"):
|
|
context.env_vars_to_clean = []
|
|
context.env_vars_to_clean.append("DATABASE_URL")
|
|
|
|
|
|
@given("no database URL is set")
|
|
def step_clear_database_url(context):
|
|
"""Clear the database URL."""
|
|
os.environ.pop("DATABASE_URL", None)
|
|
|
|
|
|
@when("I get the database URL")
|
|
def step_get_database_url(context):
|
|
"""Get the database URL."""
|
|
Settings._instance = None
|
|
if hasattr(context, "test_data_dir"):
|
|
settings = Settings(data_dir=Path(context.test_data_dir))
|
|
else:
|
|
settings = Settings()
|
|
context.database_url = settings.get_database_url()
|
|
|
|
|
|
@when('I derive the test database URL from "{url}"')
|
|
def step_derive_test_database_url(context, url):
|
|
"""Derive the fallback test database URL when none is provided."""
|
|
Settings._instance = None
|
|
settings = Settings.model_construct(database_url=url, test_database_url=None)
|
|
context.derived_database_url = settings.get_database_url(test=True)
|
|
|
|
|
|
@then('the derived test database URL should be "{expected}"')
|
|
def step_check_derived_test_database_url(context, expected):
|
|
"""Verify the derived test database URL."""
|
|
assert context.derived_database_url == expected
|
|
|
|
|
|
@when("I resolve provider defaults")
|
|
def step_resolve_provider_defaults(context):
|
|
"""Resolve provider defaults from the loaded settings."""
|
|
assert hasattr(context, "settings"), "Load settings before resolving defaults."
|
|
context.provider_defaults = context.settings.resolve_provider_defaults()
|
|
|
|
|
|
@then(
|
|
'the provider defaults should report provider "{provider}" from source "{source}"'
|
|
)
|
|
def step_verify_provider_default_provider(context, provider, source):
|
|
defaults = getattr(context, "provider_defaults", None)
|
|
assert defaults is not None, "Provider defaults must be resolved first."
|
|
assert (defaults.provider or "") == provider
|
|
assert defaults.provider_source == source
|
|
|
|
|
|
@then('the provider defaults should report model "{model}" from source "{source}"')
|
|
def step_verify_provider_default_model(context, model, source):
|
|
defaults = getattr(context, "provider_defaults", None)
|
|
assert defaults is not None, "Provider defaults must be resolved first."
|
|
assert (defaults.model or "") == model
|
|
assert defaults.model_source == source
|
|
|
|
|
|
@when('I validate provider configuration for "{provider}"')
|
|
def step_validate_provider_configuration(context, provider):
|
|
"""Validate configuration for a specific provider name."""
|
|
assert hasattr(context, "settings"), "Load settings before validation."
|
|
target = None if provider.lower() in {"none", "default", ""} else provider
|
|
context.provider_validation = context.settings.validate_provider_configuration(
|
|
target
|
|
)
|
|
|
|
|
|
@then("provider validation should fail")
|
|
def step_provider_validation_fail(context):
|
|
passed, _ = getattr(context, "provider_validation", (False, []))
|
|
assert not passed, "Expected provider validation to fail."
|
|
|
|
|
|
@then("provider validation should pass with no errors")
|
|
def step_provider_validation_pass(context):
|
|
passed, errors = getattr(context, "provider_validation", (False, []))
|
|
assert passed, "Expected provider validation to pass."
|
|
assert not errors, f"Expected no validation errors, got: {errors}"
|
|
|
|
|
|
@then('provider validation errors should include "{message}"')
|
|
def step_provider_validation_errors_include(context, message):
|
|
_, errors = getattr(context, "provider_validation", (False, []))
|
|
assert any(message in err for err in errors), (
|
|
f"Expected '{message}' in validation errors {errors}"
|
|
)
|
|
|
|
|
|
@when('I normalize the provider name "{raw}"')
|
|
def step_normalize_provider_name(context, raw):
|
|
"""Normalize a provider name through the settings helper."""
|
|
Settings._instance = None
|
|
settings = Settings.model_construct()
|
|
context.normalized_provider_name = settings._normalize_provider_name(raw)
|
|
|
|
|
|
@then("the normalized provider name should be absent")
|
|
def step_normalized_provider_absent(context):
|
|
"""Assert that the normalization result is None."""
|
|
assert hasattr(context, "normalized_provider_name"), (
|
|
"Provider normalization must be performed before assertions."
|
|
)
|
|
assert context.normalized_provider_name is None
|
|
|
|
|
|
@then('the normalized provider name should be "{expected}"')
|
|
def step_normalized_provider_value(context, expected):
|
|
"""Assert that the normalization result equals the expected value."""
|
|
assert hasattr(context, "normalized_provider_name"), (
|
|
"Provider normalization must be performed before assertions."
|
|
)
|
|
assert context.normalized_provider_name == expected
|
|
|
|
|
|
@when("I get the settings instance")
|
|
def step_get_settings(context):
|
|
"""Get the settings instance."""
|
|
context.settings1 = Settings.get_settings()
|
|
|
|
|
|
@when("I get the settings instance again")
|
|
def step_get_settings_again(context):
|
|
"""Get the settings instance again."""
|
|
context.settings2 = Settings.get_settings()
|
|
|
|
|
|
@then("both instances should be the same object")
|
|
def step_verify_singleton(context):
|
|
"""Verify both instances are the same."""
|
|
assert context.settings1 is context.settings2
|
|
|
|
|
|
@then('the {provider} API key should be "{expected}"')
|
|
def step_check_provider_api_key(context, provider, expected):
|
|
"""Check a provider API key value."""
|
|
provider_key = provider.lower().replace(" ", "_") + "_api_key"
|
|
assert getattr(context.settings, provider_key) == expected
|
|
|
|
|
|
@when("I build the LangSmith config")
|
|
def step_build_langsmith_config(context):
|
|
"""Build the LangSmith configuration payload."""
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
context.langsmith_config = settings.build_langsmith_config()
|
|
|
|
|
|
@then("the LangSmith config should be absent")
|
|
def step_assert_langsmith_config_absent(context):
|
|
"""Ensure LangSmith config is not generated when disabled."""
|
|
assert context.langsmith_config is None
|
|
|
|
|
|
@given("LangSmith is enabled without credentials")
|
|
def step_enable_langsmith_without_credentials(context):
|
|
"""Create a settings object with LangSmith enabled but missing key/project."""
|
|
Settings._instance = None
|
|
context.settings = Settings.model_construct(
|
|
langsmith_enabled=True,
|
|
langsmith_api_key=None,
|
|
langsmith_project=None,
|
|
)
|
|
|
|
|
|
@when("I validate the LangSmith configuration")
|
|
def step_validate_langsmith_configuration(context):
|
|
"""Run validation against the current LangSmith settings."""
|
|
assert hasattr(context, "settings"), "Settings must be initialized first."
|
|
context.langsmith_validation = context.settings.validate_langsmith_configuration()
|
|
|
|
|
|
@then("LangSmith validation should fail with {error_count:d} errors")
|
|
def step_assert_langsmith_validation_errors(context, error_count):
|
|
"""Assert the validation result and number of reported errors."""
|
|
enabled, errors = context.langsmith_validation
|
|
assert enabled is False
|
|
assert len(errors) == error_count
|
|
|
|
|
|
@given(
|
|
'LangSmith is configured with API key "{api_key}", project "{project}", and endpoint "{endpoint}"'
|
|
)
|
|
def step_configure_langsmith_with_endpoint(context, api_key, project, endpoint):
|
|
"""Construct LangSmith settings with explicit endpoint details."""
|
|
Settings._instance = None
|
|
context.settings = Settings.model_construct(
|
|
langsmith_enabled=True,
|
|
langsmith_api_key=api_key,
|
|
langsmith_project=project,
|
|
langsmith_endpoint=endpoint,
|
|
)
|
|
|
|
|
|
@when("I check if LangSmith is enabled")
|
|
def step_check_langsmith_enabled(context):
|
|
"""Evaluate whether LangSmith is enabled for the current settings."""
|
|
assert hasattr(context, "settings"), "Settings must be initialized first."
|
|
context.langsmith_enabled_flag = context.settings.is_langsmith_enabled
|
|
|
|
|
|
@then('the "{env_var}" environment variable should equal "{expected}"')
|
|
def step_verify_environment_variable(context, env_var, expected):
|
|
"""Verify that an environment variable has the expected value."""
|
|
assert os.environ.get(env_var) == expected
|
|
if not hasattr(context, "env_vars_to_clean"):
|
|
context.env_vars_to_clean = []
|
|
if env_var not in context.env_vars_to_clean:
|
|
context.env_vars_to_clean.append(env_var)
|
|
|
|
|
|
@given("I load singleton settings via get_settings")
|
|
@when("I load singleton settings via get_settings")
|
|
def step_load_singleton_settings_via_get_settings(context: Context) -> None:
|
|
"""Load settings through the singleton accessor and store the instance."""
|
|
settings = Settings.get_settings()
|
|
if not hasattr(context, "singleton_instances"):
|
|
context.singleton_instances = []
|
|
context.singleton_instances.append(settings)
|
|
context.settings = settings
|
|
|
|
|
|
@when("I reset singleton settings")
|
|
def step_reset_singleton_settings(context: Context) -> None:
|
|
"""Reset the singleton cache for Settings.
|
|
|
|
Uses the ``reset()`` classmethod when available, falling back to
|
|
direct ``_instance = None`` assignment to guard against Pydantic
|
|
metaclass attribute-resolution edge cases during test runs.
|
|
"""
|
|
reset_fn = Settings.__dict__.get("reset")
|
|
if reset_fn is not None:
|
|
# Unwrap the classmethod descriptor and call.
|
|
reset_fn.__func__(Settings)
|
|
else:
|
|
Settings._instance = None # type: ignore[assignment]
|
|
|
|
|
|
@then("singleton settings instances should be different")
|
|
def step_singleton_instances_should_be_different(context: Context) -> None:
|
|
"""Assert reset causes a fresh singleton instance to be constructed."""
|
|
instances = getattr(context, "singleton_instances", [])
|
|
assert len(instances) >= 2, "Expected at least two singleton instances."
|
|
assert instances[0] is not instances[-1]
|
|
|
|
|
|
@then('singleton environment should be "{expected}"')
|
|
def step_singleton_environment_should_be(context: Context, expected: str) -> None:
|
|
"""Assert the latest singleton reflects the current environment value."""
|
|
assert context.settings.environment == expected
|
|
|
|
|
|
@given('"{key}" is not set')
|
|
def step_env_var_not_set(context: Context, key: str) -> None:
|
|
"""Ensure a specific environment variable is not set."""
|
|
os.environ.pop(key, None)
|
|
if not hasattr(context, "env_vars_to_clean"):
|
|
context.env_vars_to_clean = []
|
|
# Track it so teardown knows to clean it if it gets set later
|
|
context.env_vars_to_clean.append(key)
|
|
|
|
|
|
@then('the database URL should point to "~/.cleveragents/cleveragents.db"')
|
|
def step_database_url_points_to_home(context: Context) -> None:
|
|
"""Verify the database URL resolves to the spec-required home directory path."""
|
|
expected_path = Path.home() / ".cleveragents" / "cleveragents.db"
|
|
url = context.settings.database_url
|
|
assert str(expected_path) in url, (
|
|
f"Expected database URL to contain '{expected_path}', got: {url}"
|
|
)
|
|
|
|
|
|
@then('the database URL should start with "{prefix}"')
|
|
def step_database_url_starts_with(context: Context, prefix: str) -> None:
|
|
"""Verify the database URL starts with the given prefix."""
|
|
url = context.settings.database_url
|
|
assert url.startswith(prefix), (
|
|
f"Expected database URL to start with '{prefix}', got: {url}"
|
|
)
|
|
|
|
|
|
@then('the database URL should contain "{substring}"')
|
|
def step_database_url_contains(context: Context, substring: str) -> None:
|
|
"""Verify the database URL contains the given substring."""
|
|
url = context.settings.database_url
|
|
assert substring in url, (
|
|
f"Expected database URL to contain '{substring}', got: {url}"
|
|
)
|
|
|
|
|
|
@then("the database URL should be an absolute path")
|
|
def step_database_url_is_absolute(context: Context) -> None:
|
|
"""Verify the database URL uses an absolute path (not relative)."""
|
|
url = context.settings.database_url
|
|
# For SQLite URLs: sqlite:////absolute/path or sqlite:///relative
|
|
# Absolute paths have 4 slashes: sqlite:////home/...
|
|
# Relative paths have 3 slashes: sqlite:///relative
|
|
assert url.startswith("sqlite:////") or (
|
|
url.startswith("sqlite:///") and Path(url[len("sqlite:///") :]).is_absolute()
|
|
), f"Expected database URL to use an absolute path, got: {url}"
|