# -*- coding: utf-8 -*- """`ticketchecks.PLANT_GATE_KEYS` / `plant_gate_reason` / `plant_scoped_out`, and the row-filtering `ticketqc_run.py` builds on top of them. Added 2026-09-08, owner report: a Ticket QC on another plant's job showed "a pile of MC checks" -- 6.9/6.10/6.11 (`check_lifting_points` / `check_lift_ positions` / `check_dunnage_stripping_match`) are MC plant rules, gated on knowledge keys under `lifters.lifting_points.*` that only MC's own plant layer sets, and on any other plant they printed inline as an alarming red NOT RUN row -- indistinguishable from a check that failed for a real reason on this piece. `ticketqc_run.py` itself cannot be imported on host CPython -- see `conftest.py`'s docstring and `test_reinforcing_qc.py`'s own note: it pulls in `vibe.ticketqc`, `vibe.designspec`, `pyrevit.script/forms` and more, all of which touch the Revit API at module scope. So this file tests the two PURE pieces `ticketqc_run.run()` composes -- `ticketchecks.plant_scoped_out` and `ticketpage.rows_owned_by` -- and reproduces, line for line, the same filter-then-tally the report itself runs (`r['rows'] = [row for row in r['rows'] if row[0] not in _plant_scoped_nums]` then `r['counts'] = TP.checklist_tally(r['rows'])`), so a passing test here really is proof of what the report renders. """ # pylint: disable=import-error,invalid-name,broad-except import sys import types import pytest from vibe import ticketpage as TP def _stub_revit(): if 'Autodesk.Revit.DB' in sys.modules: return [] autodesk = types.ModuleType('Autodesk') revit = types.ModuleType('Autodesk.Revit') db = types.ModuleType('Autodesk.Revit.DB') for name in ('FilteredElementCollector', 'AssemblyInstance', 'View', 'ViewSheet', 'ViewType', 'BuiltInCategory', 'Element', 'StorageType', 'ElementId', 'XYZ', 'Transform', 'Options', 'Solid', 'BoundingBoxXYZ'): setattr(db, name, type(str(name), (object,), {})) autodesk.Revit = revit revit.DB = db sys.modules['Autodesk'] = autodesk sys.modules['Autodesk.Revit'] = revit sys.modules['Autodesk.Revit.DB'] = db return ['Autodesk.Revit.DB', 'Autodesk.Revit', 'Autodesk'] _ADDED = _stub_revit() try: from vibe import ticketchecks as TC # noqa: E402 finally: for _name in _ADDED: sys.modules.pop(_name, None) # --------------------------------------------------------------------- # PLANT_GATE_KEYS / plant_gate_reason -- the check-by-check declaration. _FULLY_MC = { 'lifters': { 'lifting_points': { 'two_point_max_length_ft': 20, 'two_point_absolute_max_ft': 25, 'pci_position_tolerance_in': 6, 'dunnage_matches_stripping': True, }, }, } def test_plant_gate_keys_names_exactly_the_three_mc_rows(): """The classification this task was asked to produce, pinned so a future check gains or loses plant-scoping only as a visible edit here, the same discipline `test_reinforcing_qc.py` uses for `REINFORCING_EXCLUDED`.""" assert set(TC.PLANT_GATE_KEYS) == { 'check_lifting_points', 'check_lift_positions', 'check_dunnage_stripping_match'} def test_plant_gate_reason_is_none_for_an_ungated_check(): assert TC.plant_gate_reason('check_lifter_capacity', {}) is None assert TC.plant_gate_reason('check_lifter_capacity', _FULLY_MC) is None def test_plant_gate_reason_is_none_once_every_key_is_set(): for name in TC.PLANT_GATE_KEYS: assert TC.plant_gate_reason(name, _FULLY_MC) is None, name def test_plant_gate_reason_names_every_missing_key_on_an_empty_layer(): reason = TC.plant_gate_reason('check_lifting_points', {}) assert reason assert 'two_point_max_length_ft' in reason assert 'two_point_absolute_max_ft' in reason reason = TC.plant_gate_reason('check_lift_positions', {}) assert reason assert 'pci_position_tolerance_in' in reason reason = TC.plant_gate_reason('check_dunnage_stripping_match', {}) assert reason assert 'dunnage_matches_stripping' in reason def test_plant_gate_reason_needs_every_key_and_names_only_the_missing_one(): """6.9 needs BOTH its keys -- one set and one missing still gates the row, and the reason names the missing key without re-naming the one that is already set.""" half = {'lifters': {'lifting_points': {'two_point_max_length_ft': 20}}} reason = TC.plant_gate_reason('check_lifting_points', half) assert reason assert 'two_point_absolute_max_ft' in reason assert 'two_point_max_length_ft' not in reason def test_plant_scoped_out_is_empty_on_a_fully_configured_plant(): assert TC.plant_scoped_out(_FULLY_MC) == {} def test_plant_scoped_out_names_all_three_on_an_empty_layer(): scoped = TC.plant_scoped_out({}) assert set(scoped) == { 'check_lifting_points', 'check_lift_positions', 'check_dunnage_stripping_match'} for name, reason in scoped.items(): assert isinstance(reason, str) and reason.strip(), name def test_plant_scoped_out_is_per_check_not_all_or_nothing_globally(): """A plant that has set 6.9's keys but not 6.10/6.11's is scoped out of exactly the two it never declared -- this is not one big MC switch.""" partial = { 'lifters': { 'lifting_points': { 'two_point_max_length_ft': 20, 'two_point_absolute_max_ft': 25, }, }, } scoped = TC.plant_scoped_out(partial) assert set(scoped) == {'check_lift_positions', 'check_dunnage_stripping_match'} # --------------------------------------------------------------------- # The skip probes agree with `plant_gate_reason` -- never a second opinion. def test_probes_report_a_reason_exactly_when_plant_gate_reason_does(): t = {'lifters': [{'family': 'X'}]} # a lifter exists, so the probe # cannot defer to 6.1 instead for name, know in (('check_lifting_points', {}), ('check_lift_positions', {}), ('check_dunnage_stripping_match', {})): probe = TC._SKIP_PROBES[name] assert TC.plant_gate_reason(name, know) is not None assert probe(t, {}, know, None) is not None for name in TC.PLANT_GATE_KEYS: probe = TC._SKIP_PROBES[name] assert TC.plant_gate_reason(name, _FULLY_MC) is None # with the gate open and a lifter present, the probe defers to # piece-level silences (or None) -- never repeats the plant gate. result = probe(t, {}, _FULLY_MC, None) assert result is None or 'not set' not in (result or '') # --------------------------------------------------------------------- # `ticketpage.rows_owned_by` -- the checklist-row resolver both # `_excluded_row_numbers` (stage-scope) and `_plant_scoped_row_numbers` # (plant-scope) in `ticketqc_run.py` are thin wrappers over. def test_rows_owned_by_finds_exactly_69_610_611_on_an_empty_plant(): scoped = TC.plant_scoped_out({}) assert TP.rows_owned_by(scoped) == {'6.9', '6.10', '6.11'} def test_rows_owned_by_finds_nothing_on_a_fully_configured_plant(): scoped = TC.plant_scoped_out(_FULLY_MC) assert TP.rows_owned_by(scoped) == set() def test_rows_owned_by_is_empty_for_an_empty_names_container(): assert TP.rows_owned_by({}) == set() assert TP.rows_owned_by(set()) == set() def test_row_names_carry_the_neutral_phrase_not_mc(): """The catalogue names for 6.9/6.10/6.11 no longer say "(MC)" -- the literal client/plant tag the owner asked stripped, 2026-09-08. This is the golden that catches a regression back to it.""" names = dict((num, name) for (num, name, _g, _o, _r, _c) in TP.CHECKLIST_ITEMS if num in ('6.9', '6.10', '6.11')) assert names['6.9'] == 'Lifting-point count vs panel length (plant rule)' assert names['6.10'] == ( 'Lifting-point position vs PCI recommendation (plant rule)') assert names['6.11'] == ( 'Dunnage-point count vs stripping-point count (plant rule)') for name in names.values(): assert '(MC)' not in name # --------------------------------------------------------------------- # THE FULL PIPELINE `ticketqc_run.py` runs, reproduced exactly: # # _plant_scoped = TC.plant_scoped_out(know) # _plant_scoped_nums = TP.rows_owned_by(_plant_scoped) # r['rows'] = [row for row in r['rows'] if row[0] not in _plant_scoped_nums] # r['counts'] = TP.checklist_tally(r['rows']) # # A passing test here is proof of what the report itself does with these # three rows, not just proof the pieces exist. def _synthetic_rows(): """One NOT RUN row per plant-gated check (as `checklist_states` would render them on an empty knowledge layer, gate-closed), plus one ordinary OK row that must survive untouched.""" scoped = TC.plant_scoped_out({}) return [ ('6.9', 'Lifting-point count vs panel length (plant rule)', 'A', 'NOT RUN', scoped['check_lifting_points']), ('6.10', 'Lifting-point position vs PCI recommendation (plant rule)', 'A', 'NOT RUN', scoped['check_lift_positions']), ('6.11', 'Dunnage-point count vs stripping-point count (plant rule)', 'A', 'NOT RUN', scoped['check_dunnage_stripping_match']), ('6.1', 'Insert capacity vs load', 'A', 'OK', 'lifters clear'), ], scoped def test_a_plant_without_the_gating_keys_pulls_all_three_out_of_the_table(): rows, scoped = _synthetic_rows() scoped_nums = TP.rows_owned_by(scoped) kept = [row for row in rows if row[0] not in scoped_nums] assert [row[0] for row in kept] == ['6.1'] assert set(row[0] for row in rows) - set(row[0] for row in kept) == { '6.9', '6.10', '6.11'} def test_a_plant_without_the_gating_keys_excludes_them_from_header_counts(): rows, scoped = _synthetic_rows() scoped_nums = TP.rows_owned_by(scoped) kept = [row for row in rows if row[0] not in scoped_nums] tally = TP.checklist_tally(kept) assert tally == {'OK': 1} assert 'NOT RUN' not in tally # The unfiltered tally, for contrast: this is the leak the fix closes. unfiltered_tally = TP.checklist_tally(rows) assert unfiltered_tally.get('NOT RUN') == 3 def test_a_plant_with_the_gating_keys_keeps_all_rows_in_the_table(): scoped = TC.plant_scoped_out(_FULLY_MC) scoped_nums = TP.rows_owned_by(scoped) assert scoped_nums == set() rows = [ ('6.9', 'Lifting-point count vs panel length (plant rule)', 'A', 'OK', '2 lifting points on a 18\'-0" panel'), ('6.10', 'Lifting-point position vs PCI recommendation (plant rule)', 'A', 'OK', 'within tolerance'), ('6.11', 'Dunnage-point count vs stripping-point count (plant rule)', 'A', 'OK', 'matches stripping'), ('6.1', 'Insert capacity vs load', 'A', 'OK', 'lifters clear'), ] kept = [row for row in rows if row[0] not in scoped_nums] assert [row[0] for row in kept] == ['6.9', '6.10', '6.11', '6.1'] tally = TP.checklist_tally(kept) assert tally == {'OK': 4}