# -*- coding: utf-8 -*- """`pa_supports.support_positions` -- embeds as structural-model supports. Only the pure function is tested here (no Revit, no live model): given `ticketqc.member_boxes`-shaped boxes, a knowledge tree, and an `x_of_box` callable, does it pick the right members, dedupe, sort, and say why? A neutral family name (`PLATE_A`) stands in for the real plant plate marks (P328 / P362) -- see CLAUDE.md, no client/job/plant naming in tracked files. `read_supports` needs a live Revit `el`/`asm` and is exercised only through a bridge dry-run, not here. """ from vibe import knowledge # noqa: F401 (registers the vibe stub) from vibe import pa_supports as S def _know(families): tree = knowledge._deep_copy(knowledge.DEFAULTS) tree.setdefault('panelanalysis', {})['supports'] = {'families': families} return tree def _box(family, x): """A minimal member_boxes-shaped box; x_of_box below just echoes it.""" return {'family': family, 'mark': None, 'category': 'Specialty Equipment', 'id': 1, 'lo': [0, 0, 0], 'hi': [0, 0, 0], '_x': x} def _echo_x(box): return box['_x'] def test_matches_plant_family_and_reports_why(): know = _know(['PLATE_A']) boxes = [_box('PLATE_A', 2.0), _box('PLATE_A', 20.0)] positions, why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [2.0, 20.0] assert '2 matching member' in why assert 'PLATE_A' in why def test_match_is_case_insensitive_substring(): know = _know(['plate_a']) boxes = [_box('GPLATE_A', 3.0), _box('PLATE_A_GALV', 15.0)] positions, _why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [3.0, 15.0] def test_non_matching_families_are_ignored(): know = _know(['PLATE_A']) boxes = [_box('PLATE_A', 5.0), _box('QL062', 5.1), _box('MCC', 12.0)] positions, _why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [5.0] def test_dedupes_positions_within_quarter_foot_keeping_the_mean(): know = _know(['PLATE_A']) # 5.00 and 5.20 are 0.20 ft apart (inside DEDUPE_FT) -> one support at # their mean; 5.20 and 12.00 are far apart -> stays separate. boxes = [_box('PLATE_A', 5.00), _box('PLATE_A', 5.20), _box('PLATE_A', 12.00)] positions, why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [5.10, 12.00] assert '2 support position' in why assert '3 matching member' in why def test_dedupe_chains_through_a_group_not_just_the_first_pair(): know = _know(['PLATE_A']) # Each successive gap is 0.2 ft (< 0.25), so all four chain into one # group even though the first and last are 0.6 ft apart. boxes = [_box('PLATE_A', x) for x in (1.0, 1.2, 1.4, 1.6)] positions, _why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [1.3] def test_positions_come_back_sorted_regardless_of_box_order(): know = _know(['PLATE_A']) boxes = [_box('PLATE_A', 20.0), _box('PLATE_A', 2.0), _box('PLATE_A', 11.0)] positions, _why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [2.0, 11.0, 20.0] def test_no_match_returns_empty_list_and_names_the_reason(): know = _know(['PLATE_A']) boxes = [_box('QL062', 5.0), _box('MCC', 8.0)] positions, why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [] assert 'no member matched' in why assert 'panelanalysis.supports.families' in why assert 'PLATE_A' in why def test_empty_plant_list_returns_empty_list_and_says_so(): know = _know([]) boxes = [_box('PLATE_A', 5.0)] positions, why = S.support_positions(boxes, know, 25.0, _echo_x) assert positions == [] assert 'no member matched' in why assert 'panelanalysis.supports.families' in why def test_no_families_key_at_all_defaults_to_empty_and_no_match(): tree = knowledge._deep_copy(knowledge.DEFAULTS) boxes = [_box('PLATE_A', 5.0)] positions, why = S.support_positions(boxes, tree, 25.0, _echo_x) assert positions == [] assert 'no member matched' in why def test_empty_boxes_list_is_no_match(): know = _know(['PLATE_A']) positions, why = S.support_positions([], know, 25.0, _echo_x) assert positions == [] assert 'no member matched' in why def test_x_of_box_returning_none_is_skipped_not_fatal(): know = _know(['PLATE_A']) def flaky_x(box): return None if box['_x'] < 0 else box['_x'] boxes = [_box('PLATE_A', -1.0), _box('PLATE_A', 4.0), _box('PLATE_A', 9.0)] positions, _why = S.support_positions(boxes, know, 25.0, flaky_x) assert positions == [4.0, 9.0] def test_x_of_box_raising_is_skipped_not_fatal(): know = _know(['PLATE_A']) def raising_x(box): if box['_x'] == 'boom': raise ValueError('nope') return box['_x'] boxes = [_box('PLATE_A', 'boom'), _box('PLATE_A', 6.0)] positions, _why = S.support_positions(boxes, know, 25.0, raising_x) assert positions == [6.0]