650b1038b6
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Failing after 19s
CI / typecheck (pull_request) Failing after 56s
CI / security (pull_request) Failing after 56s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 2m4s
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / helm (pull_request) Successful in 23s
CI / quality (pull_request) Successful in 3m42s
CI / e2e_tests (pull_request) Failing after 15m14s
CI / integration_tests (pull_request) Failing after 22m3s
CI / status-check (pull_request) Failing after 1s
Introduce DomainBaseModel in src/cleveragents/domain/models/base.py that defines the single shared model_config (str_strip_whitespace, validate_assignment, arbitrary_types_allowed=False, populate_by_name, use_enum_values) previously duplicated verbatim across five domain model files. Update 14 classes across five files to inherit from DomainBaseModel instead of pydantic.BaseModel directly, removing all inline model_config duplication: - aimodelscredentials/ai_models_credentials.py (ModelProviderOption) - aimodelserrors/ai_models_errors.py (ModelError, FallbackResult) - aimodelsproviders/ai_models_providers.py (ModelProviderExtraAuthVars, ModelProviderConfigSchema) - auth/auth.py (AuthHeader, TrialPlansExceededError, TrialMessagesExceededError, BillingError, ApiError, ClientAccount, ClientAuth) - planconfig/plan_config.py (PlanConfig, ConfigSetting) Pure structural refactor — no behavioral changes. Models with different configurations (acms, aimodels_custom, etc.) are left untouched. Add BDD feature (22 scenarios) and Robot integration tests (8 test cases) verifying inheritance, config correctness, and no-duplication invariants. ISSUES CLOSED: #1941
297 lines
11 KiB
Python
297 lines
11 KiB
Python
"""Step definitions for DomainBaseModel shared configuration tests.
|
|
|
|
Only steps that are NOT already defined in other step modules are defined here.
|
|
Steps shared with aimodelscredentials_steps.py, aimodelserrors_steps.py, and
|
|
aimodelsproviders_steps.py are reused automatically by Behave's global step
|
|
registry.
|
|
"""
|
|
|
|
from behave import then, when
|
|
from pydantic import BaseModel
|
|
|
|
from cleveragents.domain.models.base import DomainBaseModel
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared expected config values
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_EXPECTED_CONFIG: dict[str, object] = {
|
|
"str_strip_whitespace": True,
|
|
"validate_assignment": True,
|
|
"arbitrary_types_allowed": False,
|
|
"populate_by_name": True,
|
|
"use_enum_values": True,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — new imports (not defined in existing step files)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I import DomainBaseModel from the domain models base module")
|
|
def step_import_domain_base_model(context: object) -> None:
|
|
"""Import DomainBaseModel and store on context."""
|
|
context.domain_base_model_class = DomainBaseModel
|
|
context.imported_class = DomainBaseModel
|
|
|
|
|
|
@when("I inspect the DomainBaseModel model_config")
|
|
def step_inspect_domain_base_model_config(context: object) -> None:
|
|
"""Store the DomainBaseModel model_config on context."""
|
|
context.inspected_config = DomainBaseModel.model_config
|
|
|
|
|
|
@when("I import ModelError from aimodelserrors")
|
|
def step_import_model_error(context: object) -> None:
|
|
"""Import ModelError."""
|
|
from cleveragents.domain.models.aimodelserrors.ai_models_errors import ModelError
|
|
|
|
context.imported_class = ModelError
|
|
|
|
|
|
@when("I import FallbackResult from aimodelserrors")
|
|
def step_import_fallback_result(context: object) -> None:
|
|
"""Import FallbackResult."""
|
|
from cleveragents.domain.models.aimodelserrors.ai_models_errors import (
|
|
FallbackResult,
|
|
)
|
|
|
|
context.imported_class = FallbackResult
|
|
|
|
|
|
@when("I import ModelProviderExtraAuthVars from aimodelsproviders")
|
|
def step_import_model_provider_extra_auth_vars(context: object) -> None:
|
|
"""Import ModelProviderExtraAuthVars."""
|
|
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
|
|
ModelProviderExtraAuthVars,
|
|
)
|
|
|
|
context.imported_class = ModelProviderExtraAuthVars
|
|
|
|
|
|
@when("I import ModelProviderConfigSchema from aimodelsproviders")
|
|
def step_import_model_provider_config_schema(context: object) -> None:
|
|
"""Import ModelProviderConfigSchema."""
|
|
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
|
|
ModelProviderConfigSchema,
|
|
)
|
|
|
|
context.imported_class = ModelProviderConfigSchema
|
|
|
|
|
|
@when("I import AuthHeader from auth")
|
|
def step_import_auth_header(context: object) -> None:
|
|
"""Import AuthHeader."""
|
|
from cleveragents.domain.models.auth.auth import AuthHeader
|
|
|
|
context.imported_class = AuthHeader
|
|
|
|
|
|
@when("I import TrialPlansExceededError from auth")
|
|
def step_import_trial_plans_exceeded_error(context: object) -> None:
|
|
"""Import TrialPlansExceededError."""
|
|
from cleveragents.domain.models.auth.auth import TrialPlansExceededError
|
|
|
|
context.imported_class = TrialPlansExceededError
|
|
|
|
|
|
@when("I import TrialMessagesExceededError from auth")
|
|
def step_import_trial_messages_exceeded_error(context: object) -> None:
|
|
"""Import TrialMessagesExceededError."""
|
|
from cleveragents.domain.models.auth.auth import TrialMessagesExceededError
|
|
|
|
context.imported_class = TrialMessagesExceededError
|
|
|
|
|
|
@when("I import BillingError from auth")
|
|
def step_import_billing_error(context: object) -> None:
|
|
"""Import BillingError."""
|
|
from cleveragents.domain.models.auth.auth import BillingError
|
|
|
|
context.imported_class = BillingError
|
|
|
|
|
|
@when("I import ApiError from auth")
|
|
def step_import_api_error(context: object) -> None:
|
|
"""Import ApiError."""
|
|
from cleveragents.domain.models.auth.auth import ApiError
|
|
|
|
context.imported_class = ApiError
|
|
|
|
|
|
@when("I import ClientAccount from auth")
|
|
def step_import_client_account(context: object) -> None:
|
|
"""Import ClientAccount."""
|
|
from cleveragents.domain.models.auth.auth import ClientAccount
|
|
|
|
context.imported_class = ClientAccount
|
|
|
|
|
|
@when("I import ClientAuth from auth")
|
|
def step_import_client_auth(context: object) -> None:
|
|
"""Import ClientAuth."""
|
|
from cleveragents.domain.models.auth.auth import ClientAuth
|
|
|
|
context.imported_class = ClientAuth
|
|
|
|
|
|
@when("I import PlanConfig from planconfig")
|
|
def step_import_plan_config(context: object) -> None:
|
|
"""Import PlanConfig."""
|
|
from cleveragents.domain.models.planconfig.plan_config import PlanConfig
|
|
|
|
context.imported_class = PlanConfig
|
|
|
|
|
|
@when("I import ConfigSetting from planconfig")
|
|
def step_import_config_setting(context: object) -> None:
|
|
"""Import ConfigSetting."""
|
|
from cleveragents.domain.models.planconfig.plan_config import ConfigSetting
|
|
|
|
context.imported_class = ConfigSetting
|
|
|
|
|
|
# When — class definition inspection
|
|
|
|
|
|
@when("I inspect the ModelProviderOption class definition")
|
|
def step_inspect_model_provider_option(context: object) -> None:
|
|
"""Store ModelProviderOption for inspection."""
|
|
from cleveragents.domain.models.aimodelscredentials.ai_models_credentials import (
|
|
ModelProviderOption,
|
|
)
|
|
|
|
context.inspected_class = ModelProviderOption
|
|
|
|
|
|
@when("I inspect the ModelError class definition")
|
|
def step_inspect_model_error(context: object) -> None:
|
|
"""Store ModelError for inspection."""
|
|
from cleveragents.domain.models.aimodelserrors.ai_models_errors import ModelError
|
|
|
|
context.inspected_class = ModelError
|
|
|
|
|
|
@when("I inspect the ModelProviderConfigSchema class definition")
|
|
def step_inspect_model_provider_config_schema_def(context: object) -> None:
|
|
"""Store ModelProviderConfigSchema for inspection."""
|
|
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
|
|
ModelProviderConfigSchema,
|
|
)
|
|
|
|
context.inspected_class = ModelProviderConfigSchema
|
|
|
|
|
|
@when("I inspect the AuthHeader class definition")
|
|
def step_inspect_auth_header(context: object) -> None:
|
|
"""Store AuthHeader for inspection."""
|
|
from cleveragents.domain.models.auth.auth import AuthHeader
|
|
|
|
context.inspected_class = AuthHeader
|
|
|
|
|
|
@when("I inspect the PlanConfig class definition")
|
|
def step_inspect_plan_config(context: object) -> None:
|
|
"""Store PlanConfig for inspection."""
|
|
from cleveragents.domain.models.planconfig.plan_config import PlanConfig
|
|
|
|
context.inspected_class = PlanConfig
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — DomainBaseModel assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the DomainBaseModel class should be available")
|
|
def step_domain_base_model_available(context: object) -> None:
|
|
"""Verify DomainBaseModel is available."""
|
|
assert context.domain_base_model_class is not None
|
|
assert context.domain_base_model_class.__name__ == "DomainBaseModel"
|
|
|
|
|
|
@then("DomainBaseModel should be a subclass of pydantic BaseModel")
|
|
def step_domain_base_model_is_pydantic(context: object) -> None:
|
|
"""Verify DomainBaseModel inherits from pydantic BaseModel."""
|
|
assert issubclass(context.domain_base_model_class, BaseModel)
|
|
|
|
|
|
@then("the DomainBaseModel str_strip_whitespace should be True")
|
|
def step_domain_base_model_str_strip(context: object) -> None:
|
|
"""Verify str_strip_whitespace on DomainBaseModel."""
|
|
assert context.inspected_config.get("str_strip_whitespace") is True
|
|
|
|
|
|
@then("the DomainBaseModel validate_assignment should be True")
|
|
def step_domain_base_model_validate_assignment(context: object) -> None:
|
|
"""Verify validate_assignment on DomainBaseModel."""
|
|
assert context.inspected_config.get("validate_assignment") is True
|
|
|
|
|
|
@then("the DomainBaseModel arbitrary_types_allowed should be False")
|
|
def step_domain_base_model_arbitrary_types(context: object) -> None:
|
|
"""Verify arbitrary_types_allowed on DomainBaseModel."""
|
|
assert context.inspected_config.get("arbitrary_types_allowed") is False
|
|
|
|
|
|
@then("the DomainBaseModel populate_by_name should be True")
|
|
def step_domain_base_model_populate_by_name(context: object) -> None:
|
|
"""Verify populate_by_name on DomainBaseModel."""
|
|
assert context.inspected_config.get("populate_by_name") is True
|
|
|
|
|
|
@then("the DomainBaseModel use_enum_values should be True")
|
|
def step_domain_base_model_use_enum_values(context: object) -> None:
|
|
"""Verify use_enum_values on DomainBaseModel."""
|
|
assert context.inspected_config.get("use_enum_values") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — subclass assertions (generic, reused for all domain models)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("{class_name} should be a subclass of DomainBaseModel")
|
|
def step_is_subclass_of_domain_base_model(context: object, class_name: str) -> None:
|
|
"""Verify the imported class is a subclass of DomainBaseModel."""
|
|
assert issubclass(context.imported_class, DomainBaseModel), (
|
|
f"{class_name} is not a subclass of DomainBaseModel"
|
|
)
|
|
|
|
|
|
@then("the {class_name} model_config should match DomainBaseModel")
|
|
def step_config_matches_domain_base_model(context: object, class_name: str) -> None:
|
|
"""Verify the imported class inherits the expected model_config values."""
|
|
cfg = context.imported_class.model_config
|
|
for key, expected in _EXPECTED_CONFIG.items():
|
|
actual = cfg.get(key)
|
|
assert actual == expected, (
|
|
f"{class_name}.model_config[{key!r}] = {actual!r}, expected {expected!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — no inline model_config duplication assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("{class_name} should not define its own model_config")
|
|
def step_no_own_model_config(context: object, class_name: str) -> None:
|
|
"""Verify the class source does not contain an inline model_config assignment.
|
|
|
|
Pydantic's metaclass copies model_config into every subclass __dict__
|
|
automatically, so we inspect the source code instead of __dict__ to
|
|
confirm no *explicit* duplication exists.
|
|
"""
|
|
import inspect
|
|
|
|
source = inspect.getsource(context.inspected_class)
|
|
# Remove the class header line so we only look at the class body
|
|
body_lines = source.splitlines()[1:]
|
|
body = "\n".join(body_lines)
|
|
assert "model_config" not in body, (
|
|
f"{class_name} has an explicit model_config in its source — "
|
|
"should inherit from DomainBaseModel without redefinition"
|
|
)
|