"""Step definitions for detail_level_coverage.feature. These steps target specific uncovered lines in detail_level.py: - Line 157: __setattr__ defense-in-depth re-freeze when model validator is bypassed - Lines 166-170: __copy__ (copy.copy) support - Line 181: __deepcopy__ with memo=None creating a fresh memo dict - Lines 250-252: register() raising ValueError when depth exceeds max_depth """ import copy import json from types import MappingProxyType from unittest.mock import patch from behave import given, then, when from pydantic import BaseModel from cleveragents.domain.models.acms.detail_level import ( DetailLevelMap, ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the detail level module is imported") def step_detail_level_module_imported(context): """Ensure the module is importable.""" assert DetailLevelMap is not None # --------------------------------------------------------------------------- # Shared construction step # --------------------------------------------------------------------------- @given( 'a DetailLevelMap with domain "{domain}" and levels {levels_json} and max_depth {max_depth:d}' ) def step_create_detail_level_map(context, domain, levels_json, max_depth): """Construct a DetailLevelMap from the scenario parameters.""" levels = json.loads(levels_json) context.dlm = DetailLevelMap( domain=domain, levels=levels, max_depth=max_depth, ) # --------------------------------------------------------------------------- # Scenario: copy.copy (lines 166-170) # --------------------------------------------------------------------------- @when("I shallow-copy the DetailLevelMap") def step_shallow_copy(context): """Perform a shallow copy via copy.copy().""" context.dlm_copy = copy.copy(context.dlm) @then("the copy should be a distinct DetailLevelMap instance") def step_verify_distinct_copy(context): """Verify the copy is a new object.""" assert context.dlm_copy is not context.dlm assert isinstance(context.dlm_copy, DetailLevelMap) @then("the copy should have the same domain and levels as the original") def step_verify_copy_data(context): """Verify the copy has identical data.""" assert context.dlm_copy.domain == context.dlm.domain assert dict(context.dlm_copy.levels) == dict(context.dlm.levels) assert context.dlm_copy.max_depth == context.dlm.max_depth # Levels should be frozen in the copy as well assert isinstance(context.dlm_copy.levels, MappingProxyType) # --------------------------------------------------------------------------- # Scenario: __deepcopy__ with None memo (line 181) # --------------------------------------------------------------------------- @when("I call __deepcopy__ with None memo") def step_deepcopy_none_memo(context): """Call __deepcopy__ directly with memo=None to hit line 181.""" context.dlm_deepcopy = context.dlm.__deepcopy__(None) @then("the deep copy should be a valid DetailLevelMap with the same data") def step_verify_deepcopy(context): """Verify the deep copy is valid and matches the original.""" assert isinstance(context.dlm_deepcopy, DetailLevelMap) assert context.dlm_deepcopy is not context.dlm assert context.dlm_deepcopy.domain == context.dlm.domain assert dict(context.dlm_deepcopy.levels) == dict(context.dlm.levels) assert context.dlm_deepcopy.max_depth == context.dlm.max_depth assert isinstance(context.dlm_deepcopy.levels, MappingProxyType) # --------------------------------------------------------------------------- # Scenario: register rejects depth > max_depth (lines 250-252) # --------------------------------------------------------------------------- @when('I register a level "{name}" with depth {depth:d}') def step_register_exceeding_depth(context, name, depth): """Attempt to register a level whose depth exceeds max_depth.""" try: context.dlm.register(name, depth) context.register_error = None except ValueError as exc: context.register_error = exc @then("a ValueError should be raised mentioning exceeds max_depth") def step_verify_register_error(context): """Verify the ValueError was raised with the expected message.""" assert context.register_error is not None, "Expected ValueError but none was raised" msg = str(context.register_error) assert "exceeds" in msg, f"Expected 'exceeds' in error message, got: {msg}" assert "max_depth" in msg, f"Expected 'max_depth' in error message, got: {msg}" # --------------------------------------------------------------------------- # Scenario: __setattr__ defense-in-depth re-freeze (line 157) # --------------------------------------------------------------------------- @when("I assign a plain dict to levels while bypassing Pydantic model validation") def step_assign_bypassing_pydantic(context): """Bypass Pydantic's model validator so __setattr__ defense-in-depth kicks in. We mock BaseModel.__setattr__ to do a plain object.__setattr__, which skips Pydantic's validate_assignment (and therefore the _freeze_levels model validator). The defense-in-depth check in DetailLevelMap.__setattr__ then detects a non-frozen dict and re-freezes it via line 157. """ def plain_setattr(self, name, value): object.__setattr__(self, name, value) with patch.object(BaseModel, "__setattr__", plain_setattr): # This calls DetailLevelMap.__setattr__ which calls the mocked # super().__setattr__ (plain assignment), then the defense check. context.dlm.levels = {"X": 1, "Y": 2} @then("levels should still be a MappingProxyType after the defense-in-depth re-freeze") def step_verify_refreeze(context): """Verify that line 157 executed and re-froze the levels.""" assert isinstance(context.dlm.levels, MappingProxyType), ( f"Expected MappingProxyType, got {type(context.dlm.levels)}" ) assert dict(context.dlm.levels) == {"X": 1, "Y": 2}