"""Step definitions for domain models testing.""" from datetime import datetime from decimal import Decimal from pathlib import Path from behave import given, then, when from pydantic import ValidationError from cleveragents.domain.models.aimodels_custom import CustomAIModel from cleveragents.domain.models.aimodelsdatamodels import AIModel, Provider from cleveragents.domain.models.core import ( Change, ChangeSet, CloudBillingFields, Context, ContextType, ContextUpdateResult, CreditsTransaction, CreditsTransactionType, CreditType, Invite, OperationType, Org, OrgRole, OrgUser, Plan, PlanStatus, Project, ProjectSettings, SummaryForUpdateContextParams, User, ) from cleveragents.domain.models.core.enums import ModelProvider from cleveragents.domain.models.orguserconfig import OrgUserConfig from cleveragents.domain.models.planconfig import PlanConfig def _sample_plan_config() -> PlanConfig: """Create a reusable PlanConfig instance for tests.""" return PlanConfig( auto_mode="manual", editor="vim", editor_command="vim", editor_args=[], editor_open_manually=False, auto_continue=False, auto_build=False, auto_update_context=False, auto_context=False, smart_context=False, auto_apply=False, auto_commit=False, skip_commit=False, can_exec=False, auto_exec=False, auto_debug=False, auto_debug_tries=0, auto_revert_on_rewind=False, skip_changes_menu=False, ) @given("I have valid project data") def step_valid_project_data(context): """Set up valid project data.""" context.project_data = { "name": "test-project", "path": Path("/tmp/test-project"), "settings": ProjectSettings(), } @given('I have project data with invalid name "{name}"') def step_invalid_project_name(context, name): """Set up project data with invalid name.""" context.project_data = { "name": name, "path": Path("/tmp/test-project"), } @when("I create a Project model") def step_create_project(context): """Create a Project model from data.""" try: context.project = Project(**context.project_data) context.error = None except ValidationError as e: context.project = None context.error = e @when("I try to create a Project model") def step_try_create_project(context): """Try to create a Project model, expecting it might fail.""" try: context.project = Project(**context.project_data) context.error = None except ValidationError as e: context.project = None context.error = e @then("the project model should be created successfully") def step_project_created(context): """Check that project was created.""" assert context.project is not None assert context.error is None @then("the project name should be validated") def step_project_name_validated(context): """Check that project name is valid.""" assert context.project.name == context.project_data["name"] @then("the project path should be absolute") def step_project_path_absolute(context): """Check that project path is absolute.""" assert context.project.path.is_absolute() @then("a validation error should be raised") def step_validation_error_raised(context): """Check that validation error was raised.""" assert context.error is not None assert isinstance(context.error, ValidationError) @then('the error should mention "{message}"') def step_error_message(context, message): """Check that error contains expected message.""" assert message in str(context.error) # Plan-related steps @given("I have valid plan data") def step_valid_plan_data(context): """Set up valid plan data.""" context.plan_data = { "project_id": 1, "name": "test-plan", "prompt": "Add error handling", "status": PlanStatus.PENDING, } @when("I create a Plan model") def step_create_plan(context): """Create a Plan model from data.""" context.plan = Plan(**context.plan_data) @then("the plan model should be created successfully") def step_plan_created(context): """Check that plan was created.""" assert context.plan is not None @then('the plan status should be "{status}"') def step_plan_status(context, status): """Check plan status.""" assert context.plan.status == status @then("the plan should have timestamps") def step_plan_timestamps(context): """Check that plan has timestamps.""" assert isinstance(context.plan.created_at, datetime) assert isinstance(context.plan.updated_at, datetime) @given('I have a plan with status "{status}"') def step_plan_with_status(context, status): """Create a plan with specific status.""" context.plan = Plan( project_id=1, name="test-plan", prompt="Test", status=PlanStatus(status), ) @when('I update the plan status to "{status}"') def step_update_plan_status(context, status): """Update plan status.""" context.plan.status = PlanStatus(status) @then('the updated plan status should be "{status}"') def step_check_updated_plan_status(context, status): """Check updated plan status.""" assert context.plan.status == status @then("the updated_at timestamp should be updated") def step_check_updated_timestamp(context): """Check that updated_at is set.""" # In real implementation, this would be handled by the repository assert context.plan.updated_at is not None # Context-related steps @given("I have valid context data") def step_valid_context_data(context): """Set up valid context data.""" context.context_data = { "plan_id": 1, "type": ContextType.FILE, "path": "/tmp/test.py", "size": 1024, } @when("I create a Context model") def step_create_context(context): """Create a Context model from data.""" context.context_obj = Context(**context.context_data) @then("the context model should be created successfully") def step_context_created(context): """Check that context was created.""" assert context.context_obj is not None @then('the context type should be "{type_name}"') def step_context_type(context, type_name): """Check context type.""" assert context.context_obj.type == type_name @then("the context should have a path") def step_context_path(context): """Check that context has a path.""" assert context.context_obj.path is not None assert len(context.context_obj.path) > 0 @given("I have context update statistics") def step_context_update_stats(context): """Prepare representative context update statistics.""" context.context_update_data = { "updatedContexts": [ { "plan_id": 1, "type": ContextType.FILE, "path": "/tmp/main.py", "size": 512, }, { "plan_id": 1, "type": ContextType.URL, "path": "https://example.com/docs", "size": 0, }, ], "tokenDiffsById": { "ctx-1": {"added": 120, "removed": 20}, "ctx-2": {"added": 0, "removed": 0}, }, "tokensDiff": 100, "totalTokens": 640, "numFiles": 1, "numUrls": 1, "numImages": 0, "numTrees": 0, "numMaps": 0, "maxTokens": 4096, } @when("I create a ContextUpdateResult summary") def step_create_context_update_result(context): """Instantiate a ContextUpdateResult model from prepared data.""" context.context_update_result = ContextUpdateResult(**context.context_update_data) @then("the context update summary should report correct totals") def step_context_update_totals(context): """Verify the aggregated counts returned by ContextUpdateResult.""" result = context.context_update_result assert len(result.updated_contexts) == 2 assert result.num_files == 1 assert result.num_urls == 1 assert result.num_images == 0 assert result.tokens_diff == 100 assert result.total_tokens == 640 @then("the token diffs should remain non-negative") def step_context_update_token_diff_non_negative(context): """Ensure nested token diff counters never drop below zero.""" result = context.context_update_result for diff_map in result.token_diffs_by_id.values(): assert all(value >= 0 for value in diff_map.values()) @given("I have summary counts for context updates") def step_summary_counts_context_updates(context): """Prepare summary counts mimicking camelCase payloads.""" context.context_summary_data = { "numFiles": 3, "numTrees": 1, "numUrls": 2, "numMaps": 0, "tokensDiff": 25, "totalTokens": 512, } @when("I create a SummaryForUpdateContextParams model") def step_create_summary_update_context_params(context): """Create SummaryForUpdateContextParams from provided counts.""" context.context_summary = SummaryForUpdateContextParams( **context.context_summary_data ) @then("the summary counts should match the source data") def step_verify_summary_update_context(context): """Validate that camelCase keys hydrate correctly into the model.""" summary = context.context_summary assert summary.num_files == 3 assert summary.num_trees == 1 assert summary.num_urls == 2 assert summary.num_maps == 0 assert summary.tokens_diff == 25 assert summary.total_tokens == 512 # Change-related steps @given("I have valid change data") def step_valid_change_data(context): """Set up valid change data.""" context.change_data = { "plan_id": 1, "file_path": "/tmp/test.py", "operation": OperationType.MODIFY, "original_content": "old content", "new_content": "new content", } @when("I create a Change model") def step_create_change(context): """Create a Change model from data.""" context.change = Change(**context.change_data) @then("the change model should be created successfully") def step_change_created(context): """Check that change was created.""" assert context.change is not None @then("the change operation should be valid") def step_change_operation_valid(context): """Check that change operation is valid.""" assert context.change.operation in OperationType @then("the change should not be applied by default") def step_change_not_applied(context): """Check that change is not applied by default.""" assert context.change.applied is False # ChangeSet-related steps @given("I have a changeset with multiple changes") def step_changeset_with_changes(context): """Create a changeset with multiple changes.""" changes = [ Change( plan_id=1, file_path="/tmp/file1.py", operation=OperationType.CREATE, new_content="new file", ), Change( plan_id=1, file_path="/tmp/file2.py", operation=OperationType.MODIFY, original_content="old", new_content="new", ), Change( plan_id=1, file_path="/tmp/file3.py", operation=OperationType.DELETE, original_content="deleted content", ), ] context.changeset = ChangeSet(plan_id=1, changes=changes) @when("I get the changeset statistics") def step_get_changeset_stats(context): """Get statistics from changeset.""" context.stats = context.changeset.stats @then("the stats should show correct counts") def step_stats_correct_counts(context): """Check that stats show correct counts.""" assert context.stats["total"] == 3 assert context.stats["creates"] == 1 assert context.stats["modifies"] == 1 assert context.stats["deletes"] == 1 @then("the stats should count creates, modifies, and deletes") def step_stats_count_operations(context): """Check that stats count all operation types.""" assert "creates" in context.stats assert "modifies" in context.stats assert "deletes" in context.stats assert "applied" in context.stats # Domain models from cloud/organization data def _sample_billing_payload() -> dict[str, object]: """Provide representative camelCase billing payload.""" return { "creditsBalance": "125.5", "monthlyGrant": "200.0", "autoRebuyEnabled": True, "autoRebuyMinThreshold": "5.0", "autoRebuyToBalance": "50.0", "notifyThreshold": "10.0", "maxThresholdPerMonth": "500.0", "billingCycleStartedAt": "2025-01-01T00:00:00Z", "changedBillingMode": False, "trialPaid": True, "stripeSubscriptionId": "sub-123", "subscriptionStatus": "active", "subscriptionPausedAt": "2025-01-10T00:00:00Z", "stripePaymentMethod": "pm_abc", "subscriptionActionRequired": False, "subscriptionActionRequiredInvoiceUrl": None, } @given("I have sample cloud billing data") def step_sample_cloud_billing(context): """Prepare representative cloud billing data.""" context.cloud_billing_data = _sample_billing_payload() @when("I create a CloudBillingFields model") def step_create_billing_fields(context): """Instantiate CloudBillingFields from camelCase payload.""" context.billing_fields = CloudBillingFields(**context.cloud_billing_data) @then("the billing fields should expose decimal values") def step_verify_billing_fields(context): """Validate decimal conversion for billing fields.""" billing = context.billing_fields assert billing.credits_balance == Decimal("125.5") assert billing.monthly_grant == Decimal("200.0") assert billing.subscription_action_required is False @given("I have organization data with billing") def step_org_with_billing(context): """Prepare organization payload referencing billing fields.""" context.org_payload = { "id": "org-1", "name": "Example Org", "isTrial": True, "autoAddDomainUsers": True, "integratedModelsMode": False, } context.org_billing_payload = _sample_billing_payload() @when("I create an Org domain model") def step_create_org_model(context): """Instantiate Org with nested billing fields.""" billing = CloudBillingFields(**context.org_billing_payload) context.org_model = Org(cloud_billing_fields=billing, **context.org_payload) @then("the org should include billing configuration") def step_verify_org_model(context): """Ensure the org retains billing configuration.""" org = context.org_model assert org.cloud_billing_fields is not None assert org.cloud_billing_fields.notify_threshold == Decimal("10.0") @given("I have an org role definition") def step_org_role_definition(context): """Set up org role payload.""" context.org_role_data = { "id": "role-1", "isDefault": True, "label": "Owner", "description": "Full access", } @when("I create an OrgRole model") def step_create_org_role(context): """Instantiate OrgRole.""" context.org_role = OrgRole(**context.org_role_data) @then("the org role should retain its label") def step_verify_org_role(context): """Ensure org role values persist.""" assert context.org_role.label == "Owner" assert context.org_role.is_default is True @given("I have user data referencing a default plan config") def step_user_with_plan_config(context): """Prepare user payload referencing plan config.""" context.user_plan_config = _sample_plan_config() context.user_data = { "id": "user-1", "name": "Casey", "email": "casey@example.com", "isTrial": False, "numNonDraftPlans": 3, } @when("I create a User model") def step_create_user_model(context): """Instantiate User with nested plan config.""" context.user_model = User( default_plan_config=context.user_plan_config, **context.user_data ) @then("the user should include the default plan config") def step_verify_user_model(context): """Ensure user retains plan config.""" assert context.user_model.default_plan_config is not None assert context.user_model.default_plan_config.auto_mode == "manual" @given("I have an org user association") def step_org_user_association(context): """Prepare OrgUser payload.""" context.org_user_config = OrgUserConfig( prompted_claude_max=False, use_claude_subscription=True, claude_subscription_cooldown_started_at=datetime(2025, 1, 1, 0, 0, 0), ) context.org_user_data = { "orgId": "org-1", "userId": "user-1", "orgRoleId": "role-1", } @when("I create an OrgUser model") def step_create_org_user(context): """Instantiate OrgUser.""" context.org_user_model = OrgUser( config=context.org_user_config, **context.org_user_data ) @then("the org user config should be attached") def step_verify_org_user(context): """Ensure OrgUser carries config payload.""" assert context.org_user_model.config is not None assert context.org_user_model.config.use_claude_subscription is True @given("I have invite metadata") def step_invite_metadata(context): """Prepare Invite payload.""" context.invite_data = { "id": "invite-1", "orgId": "org-1", "email": "new@example.com", "name": "New User", "orgRoleId": "role-1", "inviterId": "user-1", "inviteeId": None, "acceptedAt": "2025-02-01T12:00:00Z", "createdAt": "2025-01-15T08:30:00Z", } @when("I create an Invite model") def step_create_invite(context): """Instantiate Invite.""" context.invite_model = Invite(**context.invite_data) @then("the invite should track acceptance timestamps") def step_verify_invite(context): """Ensure Invite datetimes parsed.""" assert isinstance(context.invite_model.accepted_at, datetime) assert context.invite_model.accepted_at.year == 2025 @given("I have credits transaction data") def step_credits_transaction_data(context): """Prepare CreditsTransaction payload.""" context.credits_transaction_data = { "id": "txn-1", "orgId": "org-1", "orgName": "Example Org", "userId": "user-1", "userEmail": "casey@example.com", "userName": "Casey", "transactionType": CreditsTransactionType.CREDIT, "amount": "42.0", "startBalance": "20.0", "endBalance": "62.0", "creditType": CreditType.TRIAL, "creditIsAutoRebuy": True, "creditAutoRebuyMinThreshold": "5.0", "creditAutoRebuyToBalance": "40.0", "debitModelProvider": ModelProvider.OPENAI, "debitModelRole": "ModelRolePlanner", "debitModelName": "gpt-4.1", "debitPlanId": "plan-1", "debitPlanName": "Core Plan", "createdAt": "2025-01-20T00:00:00Z", } @when("I create a CreditsTransaction model") def step_create_credits_transaction(context): """Instantiate CreditsTransaction.""" context.credits_transaction = CreditsTransaction(**context.credits_transaction_data) @then("the credits transaction should track balances") def step_verify_credits_transaction(context): """Ensure CreditsTransaction stores decimals and provider metadata.""" tx = context.credits_transaction assert tx.amount == Decimal("42.0") assert tx.debit_model_provider == ModelProvider.OPENAI assert tx.end_balance == Decimal("62.0") # Auto-generated models steps @when("I import the auto-generated models") def step_import_autogenerated(context): """Import auto-generated models.""" try: from cleveragents.domain.models.auth import AuthHeader from cleveragents.domain.models.planconfig import PlanConfig from cleveragents.domain.models.stream import BuildInfo context.import_error = None context.imported_models = [AuthHeader, BuildInfo, PlanConfig] except Exception as e: context.import_error = e context.imported_models = [] @then("all models should load without errors") def step_models_load_without_errors(context): """Check that models loaded without errors.""" assert context.import_error is None assert len(context.imported_models) > 0 @then("the models should have proper Pydantic configuration") def step_models_have_pydantic_config(context): """Check that models have Pydantic configuration.""" for model_class in context.imported_models: assert hasattr(model_class, "model_config") assert hasattr(model_class, "model_validate") @given('I create an "{model_name}" with the following data:') @given('I create a "{model_name}" with the following data:') def create_model(context, model_name): data = {row["name"]: row["value"] for row in context.table} if model_name == "AIModel": context.model = AIModel(**data) elif model_name == "Provider": context.model = Provider(**data) elif model_name == "CustomAIModel": context.model = CustomAIModel(**data) @then('the "{model_name}" object should have the following attributes:') def verify_model_attributes(context, model_name): for row in context.table: attr_name = row["name"] expected_value = row["value"] actual_value = getattr(context.model, attr_name) if expected_value == "False": expected_value = False assert actual_value == expected_value, ( f"Expected {attr_name} to be {expected_value}, but got {actual_value}" )