"""Behave steps for UKO Layer 2 detail-level map tests. Covers DetailLevelMap base maps, DetailLevelMapBuilder, ``build_effective_map``, and unknown-level resolution. All step definitions use the ``for uko_l2`` suffix to avoid AmbiguousStep collisions with other feature files. """ from __future__ import annotations import copy from types import MappingProxyType from _uko_l2_test_helpers import capture_error from behave import given, then, when from behave.runner import Context from cleveragents.acms.uko.detail_level_maps import ( CODE_DETAIL_LEVEL_MAP, FUNC_DETAIL_LEVEL_MAP, OO_DETAIL_LEVEL_MAP, PROC_DETAIL_LEVEL_MAP, DetailLevelMapBuilder, build_effective_map, ) from cleveragents.domain.models.acms.crp import ( DetailLevelCycleError, DetailLevelMap, ) # --------------------------------------------------------------------------- # DetailLevelMap -- base maps # --------------------------------------------------------------------------- @given("the uko-code detail level map for uko_l2") def step_get_code_map(ctx: Context) -> None: ctx.detail_map = CODE_DETAIL_LEVEL_MAP @given("the uko-oo detail level map for uko_l2") def step_get_oo_map(ctx: Context) -> None: ctx.detail_map = OO_DETAIL_LEVEL_MAP @given("the uko-func detail level map for uko_l2") def step_get_func_map(ctx: Context) -> None: ctx.detail_map = FUNC_DETAIL_LEVEL_MAP @given("the uko-proc detail level map for uko_l2") def step_get_proc_map(ctx: Context) -> None: ctx.detail_map = PROC_DETAIL_LEVEL_MAP @then('the map should have domain "{expected}" for uko_l2') def step_check_map_domain(ctx: Context, expected: str) -> None: assert ctx.detail_map.domain == expected @then("the map should have max_depth {expected:d} for uko_l2") def step_check_map_max_depth(ctx: Context, expected: int) -> None: assert ctx.detail_map.max_depth == expected @then('the map should resolve "{name}" to {expected:d} for uko_l2') def step_check_map_resolve_name(ctx: Context, name: str, expected: int) -> None: assert ctx.detail_map.resolve(name) == expected @then("the map should have {count:d} levels for uko_l2") def step_check_map_level_count(ctx: Context, count: int) -> None: assert len(ctx.detail_map.effective_levels()) == count @then("the map should resolve integer {value:d} to {expected:d} for uko_l2") def step_check_map_resolve_int(ctx: Context, value: int, expected: int) -> None: assert ctx.detail_map.resolve(value) == expected @then('the map parent domain should be "{expected}" for uko_l2') def step_check_map_parent_domain(ctx: Context, expected: str) -> None: assert ctx.detail_map.parent is not None assert ctx.detail_map.parent.domain == expected # --------------------------------------------------------------------------- # DetailLevelMapBuilder # --------------------------------------------------------------------------- @given( 'a detail level map builder with parent "{parent}" and domain "{domain}" for uko_l2' ) def step_create_builder(ctx: Context, parent: str, domain: str) -> None: parent_map = CODE_DETAIL_LEVEL_MAP if parent == "uko-code:" else None assert parent_map is not None, f"Unknown parent: {parent}" ctx.builder = DetailLevelMapBuilder(parent_map, domain) @when('I insert "{name}" after "{after}" for uko_l2') def step_builder_insert(ctx: Context, name: str, after: str) -> None: ctx.builder.insert_after(after, name) @when("I build the map for uko_l2") def step_builder_build(ctx: Context) -> None: ctx.built_map = ctx.builder.build() @then('the built map should have domain "{expected}" for uko_l2') def step_check_built_domain(ctx: Context, expected: str) -> None: assert ctx.built_map.domain == expected @then('the built map should resolve "{name}" to {expected:d} for uko_l2') def step_check_built_resolve(ctx: Context, name: str, expected: int) -> None: assert ctx.built_map.resolve(name) == expected @then("the built map should have {count:d} levels for uko_l2") def step_check_built_levels(ctx: Context, count: int) -> None: assert len(ctx.built_map.effective_levels()) == count @when('I try to insert after non-existent level "{name}" for uko_l2') def step_insert_nonexistent(ctx: Context, name: str) -> None: with capture_error(ctx, ValueError): ctx.builder.insert_after(name, "TEST") @then("a value error should be raised for uko_l2") def step_check_value_error(ctx: Context) -> None: assert ctx.error is not None assert isinstance(ctx.error, ValueError) @when("I try to create a builder with empty domain for uko_l2") def step_create_builder_empty_domain(ctx: Context) -> None: with capture_error(ctx, ValueError): DetailLevelMapBuilder(CODE_DETAIL_LEVEL_MAP, "") @when('I try to insert empty level name after "{after}" for uko_l2') def step_insert_empty_name(ctx: Context, after: str) -> None: with capture_error(ctx, ValueError): ctx.builder.insert_after(after, "") @when('I try to insert "{name}" after empty level name for uko_l2') def step_insert_after_empty(ctx: Context, name: str) -> None: with capture_error(ctx, ValueError): ctx.builder.insert_after("", name) @then('the builder parent domain should be "{expected}" for uko_l2') def step_check_builder_parent(ctx: Context, expected: str) -> None: assert ctx.builder.parent.domain == expected @then('the builder domain should be "{expected}" for uko_l2') def step_check_builder_domain(ctx: Context, expected: str) -> None: assert ctx.builder.domain == expected # --------------------------------------------------------------------------- # build_effective_map function # --------------------------------------------------------------------------- @when("I build an effective map with no insertions for uko_l2") def step_build_effective_no_insert(ctx: Context) -> None: ctx.effective = build_effective_map(ctx.detail_map, []) @when('I build an effective map with insertion "{name}" after "{after}" for uko_l2') def step_build_effective_insert(ctx: Context, name: str, after: str) -> None: ctx.effective = build_effective_map(ctx.detail_map, [(after, name)]) @when('I try to build an effective map with insertion after "{after}" for uko_l2') def step_build_effective_invalid(ctx: Context, after: str) -> None: with capture_error(ctx, ValueError): build_effective_map(ctx.detail_map, [(after, "TEST")]) @then("the effective map should have {count:d} entries for uko_l2") def step_check_effective_count(ctx: Context, count: int) -> None: assert len(ctx.effective) == count @then( 'the effective map entry {index:d} should be "{name}" at depth {depth:d} for uko_l2' ) def step_check_effective_entry(ctx: Context, index: int, name: str, depth: int) -> None: entry_name, entry_depth = ctx.effective[index] assert entry_name == name, f"Expected '{name}' but got '{entry_name}'" assert entry_depth == depth, f"Expected depth {depth} but got {entry_depth}" # --------------------------------------------------------------------------- # Unknown level resolution # --------------------------------------------------------------------------- @when('I try to resolve unknown level "{name}" for uko_l2') def step_resolve_unknown(ctx: Context, name: str) -> None: with capture_error(ctx, ValueError): ctx.detail_map.resolve(name) # --------------------------------------------------------------------------- # Fresh DetailLevelMap (register / mutate) # --------------------------------------------------------------------------- @given("a fresh detail level map for uko_l2") def step_create_fresh_map(ctx: Context) -> None: ctx.fresh_map = DetailLevelMap( domain="test:", parent=None, levels={"BASE": 0}, max_depth=10 ) @when('I register level "{name}" with value {value:d} for uko_l2') def step_register_level(ctx: Context, name: str, value: int) -> None: ctx.fresh_map.register(name, value) @then('the fresh map should resolve "{name}" to {expected:d} for uko_l2') def step_check_fresh_resolve(ctx: Context, name: str, expected: int) -> None: assert ctx.fresh_map.resolve(name) == expected @when('I try to register level "{name}" with value {value:d} for uko_l2') def step_try_register_negative(ctx: Context, name: str, value: int) -> None: with capture_error(ctx, ValueError): ctx.fresh_map.register(name, value) @when("I try to mutate the levels dict for uko_l2") def step_mutate_levels(ctx: Context) -> None: with capture_error(ctx, TypeError): ctx.detail_map.levels["HACK"] = 99 @then("a type error should be raised for uko_l2") def step_check_type_error(ctx: Context) -> None: assert ctx.error is not None assert isinstance(ctx.error, TypeError) @when('I try to insert duplicate level "{name}" after "{after}" for uko_l2') def step_try_insert_duplicate(ctx: Context, name: str, after: str) -> None: with capture_error(ctx, ValueError): ctx.builder.insert_after(after, name) @when("I try to build an effective map with duplicate level for uko_l2") def step_build_effective_duplicate(ctx: Context) -> None: with capture_error(ctx, ValueError): build_effective_map(ctx.detail_map, [("MEMBER_LISTING", "MODULE_LISTING")]) @when("I try to mutate effective_levels for uko_l2") def step_mutate_effective_levels(ctx: Context) -> None: with capture_error(ctx, TypeError): effective = ctx.detail_map.effective_levels() effective["HACK"] = 99 # type: ignore[index] # deliberate mutation test # --------------------------------------------------------------------------- # Review findings: H3 — MappingProxyType serialization # --------------------------------------------------------------------------- @when("I serialize the detail level map to JSON for uko_l2") def step_serialize_map_json(ctx: Context) -> None: ctx.serialized_json = ctx.detail_map.model_dump_json() @then('the serialized JSON should contain "{expected}" for uko_l2') def step_check_serialized_json(ctx: Context, expected: str) -> None: assert expected in ctx.serialized_json @when("I dump the detail level map to dict for uko_l2") def step_dump_map_dict(ctx: Context) -> None: ctx.dumped_dict = ctx.detail_map.model_dump() @then('the dumped dict levels should contain "{expected}" for uko_l2') def step_check_dumped_dict(ctx: Context, expected: str) -> None: assert expected in ctx.dumped_dict["levels"] # --------------------------------------------------------------------------- # Review findings: M5 — cycle guard in resolve() # --------------------------------------------------------------------------- @given("a detail level map with circular parent for uko_l2") def step_create_circular_map(ctx: Context) -> None: # Build two maps that point to each other map_a = DetailLevelMap(domain="cycle-a:", parent=None, levels={"A": 0}, max_depth=5) map_b = DetailLevelMap( domain="cycle-b:", parent=map_a, levels={"B": 1}, max_depth=5 ) # Introduce cycle: map_a.parent -> map_b object.__setattr__(map_a, "parent", map_b) ctx.circular_map = map_a @when('I try to resolve level "{name}" on the circular map for uko_l2') def step_resolve_circular(ctx: Context, name: str) -> None: with capture_error(ctx, DetailLevelCycleError): ctx.circular_map.resolve(name) @then("a cycle error should be raised for uko_l2") def step_check_cycle_error(ctx: Context) -> None: assert ctx.error is not None assert isinstance(ctx.error, DetailLevelCycleError) # --------------------------------------------------------------------------- # Review findings: H2 — levels re-frozen on assignment # --------------------------------------------------------------------------- @when("I assign a dict to the levels field for uko_l2") def step_assign_dict_levels(ctx: Context) -> None: ctx.fresh_map.levels = {"NEW_LEVEL": 3} # type: ignore[assignment] # deliberate test ctx.levels_type = type(ctx.fresh_map.levels) ctx.is_mapping_proxy = isinstance(ctx.fresh_map.levels, MappingProxyType) @then("the levels should still be an immutable MappingProxy for uko_l2") def step_check_levels_refrozen(ctx: Context) -> None: assert ctx.is_mapping_proxy, f"Expected MappingProxyType but got {ctx.levels_type}" # Also verify it's actually immutable. # Note: capture_error() is not used here because the pattern is # "assert that an exception IS raised" (inline assertion), not # "capture the error for a later @then step". try: ctx.fresh_map.levels["HACK"] = 99 # type: ignore[index] # deliberate mutation test msg = "Expected TypeError on mutation" raise AssertionError(msg) except TypeError: pass # --------------------------------------------------------------------------- # Review findings: F7 — cycle guard in effective_levels() # --------------------------------------------------------------------------- @when("I try to get effective levels on the circular map for uko_l2") def step_effective_levels_circular(ctx: Context) -> None: with capture_error(ctx, DetailLevelCycleError): ctx.circular_map.effective_levels() # --------------------------------------------------------------------------- # Review findings: F13 — deepcopy support # --------------------------------------------------------------------------- @when("I deep copy the detail level map for uko_l2") def step_deep_copy_map(ctx: Context) -> None: ctx.copied_map = copy.deepcopy(ctx.detail_map) @then('the copied map should have domain "{expected}" for uko_l2') def step_check_copied_domain(ctx: Context, expected: str) -> None: assert ctx.copied_map.domain == expected @then('the copied map should resolve "{name}" to {expected:d} for uko_l2') def step_check_copied_resolve(ctx: Context, name: str, expected: int) -> None: assert ctx.copied_map.resolve(name) == expected @then("the copied map should not be the same object for uko_l2") def step_check_copied_identity(ctx: Context) -> None: assert ctx.copied_map is not ctx.detail_map