forked from HAL9000/cleveragents-core
387 lines
14 KiB
Python
387 lines
14 KiB
Python
"""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
|