8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
277 lines
11 KiB
Python
277 lines
11 KiB
Python
# -*- 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}
|