Files
willtools-demo/WillTools.extension/tests/test_ticketpage.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:32:15 -05:00

4129 lines
172 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Phase 2: the arithmetic behind the repointed legend checks.
The ticket-side strand count is recovered from the BOM's linear-feet row by
division, never read as a quantity (249 on a 24'-11" piece is 9.99 strands,
and the section prints (10)). f'c/f'ci compare the title-block sheet params
against the design. The BOM / section / model three-way agreement is what
LGD's replacement checks against. All pure arithmetic: no Revit, no model.
"""
# pylint: disable=import-error,invalid-name,broad-except
import re
from vibe import ticketpage
# ------------------------------------------------ BOM linear-feet -> count
def test_linear_ft_divides_to_the_printed_count():
"""249 ft on 24.9167 ft is 9.99 strands; the section prints (10)."""
assert ticketpage.strand_count_from_linear_ft(249, 24.9167) == 10
assert ticketpage.strand_count_from_linear_ft(321, 22.9167) == 14
assert ticketpage.strand_count_from_linear_ft(349, 24.9167) == 14
assert ticketpage.strand_count_from_linear_ft(138, 22.9167) == 6
def test_linear_ft_handles_unusable_inputs():
assert ticketpage.strand_count_from_linear_ft(None, 24.9167) is None
assert ticketpage.strand_count_from_linear_ft(249, None) is None
assert ticketpage.strand_count_from_linear_ft(249, 0) is None
assert ticketpage.strand_count_from_linear_ft('x', 24.9167) is None
def test_linear_ft_measured_remainders_pass():
"""Twelve real tickets' ratios sit 0.0032 .. 0.0226 off a whole number;
all of them must still recover a count (measured 2026-08-27)."""
measured = [ # (linear_ft, length_ft, expected strands)
(321.0, 22.9167, 14), # WP199
(138.0, 22.9167, 6), # WP200
(361.0, 25.7917, 14), # WP194
(349.0, 24.9167, 14), # WP195
(349.0, 24.9167, 14), # WP196
(265.0, 18.9167, 14), # WP215
(249.0, 24.9167, 10), # WP210
(185.0, 18.4583, 10), # WP219
(185.0, 18.4583, 10), # WP220
(307.0, 21.9583, 14), # WP213
(132.0, 21.9583, 6), # WP214
(349.0, 24.9167, 14), # WP412
]
for linear, length, want in measured:
got = ticketpage.strand_count_from_linear_ft(linear, length)
assert got == want, (linear, length, got)
def test_linear_ft_rejects_a_ratio_far_from_whole():
"""A row that is NOT a length in feet must come back None, never a
confident wrong count. Review cases: a row that is actually a count
(10 / 24.9 ~ 0.40), and a misread unit (249 / 2.49 ~ 99.93)."""
assert ticketpage.strand_count_from_linear_ft(10, 24.9167) is None
assert ticketpage.strand_count_from_linear_ft(249, 2.49167) is None
# borderline: exactly at tolerance is still usable
assert ticketpage.strand_count_from_linear_ft(249.0 * 1.05, 24.9167) is None
# ------------------------------------------------------------- strengths
def test_strengths_agree_within_tolerance():
ok, what = ticketpage.strengths_agree(6000, 6000)
assert ok
assert '6,000' in what
ok, _ = ticketpage.strengths_agree(6000, 5999.6) # within 0.5 psi
assert ok
def test_strengths_disagree_and_missing_values():
ok, _ = ticketpage.strengths_agree(6000, 3500)
assert not ok
# summary absent -> nothing to check, agree trivially
ok, _ = ticketpage.strengths_agree(None, 3500)
assert ok
# ticket value absent -> caller decides what "no value" means
ok, what = ticketpage.strengths_agree(6000, None)
assert not ok
assert 'no value' in what
# ---------------------------------------------------- three-way agreement
def test_count_agrees_bom_model_and_section():
ok, _ = ticketpage.count_agrees(10, 10, 10)
assert ok
ok, _ = ticketpage.count_agrees(10, 10, None) # section not read
assert ok
ok, _ = ticketpage.count_agrees(0, 0, 0) # no strands on piece
assert ok
def test_count_agrees_surfaces_each_disagreement():
ok, what = ticketpage.count_agrees(10, 12, None)
assert not ok
assert 'BOM implies' in what
ok, what = ticketpage.count_agrees(None, 12, None)
assert not ok
assert 'unusable' in what
ok, what = ticketpage.count_agrees(10, 10, 8)
assert not ok
assert 'section prints' in what
ok, what = ticketpage.count_agrees(12, None, None)
assert not ok
assert 'model' in what
# ------------------------------------------------- handling stations
def test_stations_from_ends_wp412():
"""WP412 prints 2'-6\" 7'-6\" [blank] 7'-6\" 2'-6\" on 24'-11\"; the
recovered stations are the four modelled P136 to the inch (audit)."""
ft = [2.5, 7.5, 7.5, 2.5] # blank dropped from the middle
got = ticketpage.stations_from_ends(24.9167, ft)
assert got == [2.5, 10.0, 14.9167, 22.4167]
def test_stations_from_ends_wp213():
"""WP213 prints 2'-0\" 6'-0\" [blank] 7'-4\" 2'-0\" on 21'-11\"."""
ft = [2.0, 6.0, 7.3333, 2.0]
got = ticketpage.stations_from_ends(21.9583, ft)
assert got == [2.0, 8.0, 12.625, 19.9583]
def test_stations_from_ends_odd_count_is_symmetric():
"""The diagram [2,6,5,2] on 10: segments from the left give 2,8; from
the right end they give 8 (the right 2) and 3 (2+5 in from that end)."""
got = ticketpage.stations_from_ends(10.0, [2.0, 6.0, 5.0, 2.0])
assert sorted(got) == [2.0, 3.0, 8.0]
def test_dunnage_stations_each_end():
got = ticketpage.dunnage_stations([4.75], length_ft=22.9167)
assert got == [4.75, 18.1667]
def test_dunnage_stations_printing_order_wp199_new():
"""WP199 NEW prints 5'-0\" (left) and 3'-0\" (right). Printing order:
first inset from the left, second from the right. Mirrored = false."""
got = ticketpage.dunnage_stations([5.0, 3.0], length_ft=22.9167)
assert got == [5.0, 19.9167]
def test_dunnage_clearance_wp199_measured_cases():
"""Claude's six measured rows, straight from the model. WP199 really
has THREE openings -- the 0.0-0.667 end notch is what gives station
4.75 its 49.0\" clearance (nearest edge 0.667, not the far opening).
Openings are full (x0,x1,y0,y1) rectangles; piece_width is held at
the real values (WP199 11.92, WP213 11.92) so the 19% corner notches
stay OUT of the judgement (they do not take the support)."""
wp199_openings = [
(0.0, 0.667, 11.71, 11.917), # 4.3%-wide corner notch: no
(15.60, 18.96, 1.00, 9.77), # 73.6%-wide real opening
(22.25, 22.917, 11.71, 11.917), # corner notch: no
]
wp213_openings = [
(13.60, 16.96, 1.00, 9.77),
(18.63, 19.29, 1.00, 9.77),
]
# WP199: only OPENING 1 (index 1) is a through-thickness opening
# (measured by raycast: 0.0 in concrete through it); the corner pockets
# are blind recesses. WP213: both openings are through.
cases = [
# (label, station, openings, relevant, expect_sev, expect_dist)
('WP199 OLD 4.75', 4.75, wp199_openings, [1], 'OK', 130.2),
('WP199 OLD 18.17', 18.17, wp199_openings, [1], 'FAIL', 0.0),
('WP199 NEW 5.00', 5.00, wp199_openings, [1], 'OK', 127.2),
('WP199 NEW 19.92', 19.92, wp199_openings, [1], 'OK', 11.5),
('WP199 MIR 17.92', 17.92, wp199_openings, [1], 'FAIL', 0.0),
('WP213 OLD 17.38', 17.38, wp213_openings, [0, 1], 'WARN', 5.0),
('WP213 NEW 17.96', 17.96, wp213_openings, [0, 1], 'OK', 8.0),
('WP213 MIR 16.96', 16.96, wp213_openings, [0, 1], 'FAIL', 0.0),
]
for label, station, openings, relevant, want_sev, want_in in cases:
sev, name, dist = ticketpage.dunnage_clearance(
station, openings, relevant, fail_in=3.0, warn_in=6.0)
assert sev == want_sev, label
assert dist == want_in, label
if sev == 'FAIL':
assert name in ('INSIDE', 'OPENING 1', 'OPENING 2',
'OPENING 3'), label
def test_dunnage_clearance_spandrel_notch_is_not_a_hole():
"""WP200: a top-edge 11\" notch cutting only 19% of the width leaves
the block on 81% of the concrete -- it must NOT fail. The length-axis
filter reported 'sits INSIDE an opening' here; that was the false
positive Claude caught (measured: WP200 notch = 19.0% of width vs the
real 73.6% of WP199's opening)."""
wp200_notch = (2.79, 20.96, 3.92, 4.83) # 0.92 ft across on 4.83
openings = [wp200_notch, (0.0, 0.667, 4.62, 4.83),
(22.25, 22.917, 4.62, 4.83)]
# The notch is a blind recess (3.5 in of concrete through it, measured
# by raycast) -- NOT in relevant, so it is neither INSIDE nor a
# distance source. The corner pockets are irrelevant too; relevant is
# empty, so nothing to judge.
sev, name, dist = ticketpage.dunnage_clearance(
4.75, openings, [], fail_in=3.0, warn_in=6.0)
assert sev == 'OK', (sev, name, dist)
# same notch, station right at the length overlap: still clear
sev, name, dist = ticketpage.dunnage_clearance(
18.17, openings, [], fail_in=3.0, warn_in=6.0)
assert sev == 'OK', (sev, name, dist)
def test_dunnage_clearance_relevance_filters_distance_too():
"""Claude's latent bug: a 1.7%-wide corner pocket reported WARN at 5
inches and FAIL at 1 inch because the edge-distance branch skipped the
relevance filter. An irrelevant opening is neither INSIDE nor a
distance source."""
notch = (0.0, 0.667, 0.655, 0.667) # 1.7% of an 8 ft width
openings = [notch, (6.0, 8.0, 0.0, 8.0)] # + a real full-width hole
# station 1.0 is 4 inches from the notch's far edge (0.667) -- the old
# code would WARN; with relevance on the distance branch it is only
# judged against the real hole (far away) -> OK
sev, name, dist = ticketpage.dunnage_clearance(
1.0, openings, [1], fail_in=3.0, warn_in=6.0)
assert sev == 'OK', (sev, name, dist)
# real hole still triggers: station inside it
sev, name, dist = ticketpage.dunnage_clearance(
7.0, openings, [1], fail_in=3.0, warn_in=6.0)
assert sev == 'FAIL' and name == 'INSIDE', (sev, name)
def test_dunnage_clearance_edges():
"""Exactly at the fail/warn boundaries: fail < 3, warn 3..<6, ok >= 6.
The opening here is relevant (through), so it counts at every station."""
openings = [(10.0, 12.0, 0.0, 8.0)]
sev, _, d = ticketpage.dunnage_clearance(13.0, openings, [0],
fail_in=3.0, warn_in=6.0)
assert sev == 'OK' # 12" away
sev, _, d = ticketpage.dunnage_clearance(12.5, openings, [0],
fail_in=3.0, warn_in=6.0)
assert sev == 'OK' # 6" away -> OK (>= 6)
sev, _, d = ticketpage.dunnage_clearance(12.4, openings, [0],
fail_in=3.0, warn_in=6.0)
assert sev == 'WARN' # 4.8" away
sev, _, d = ticketpage.dunnage_clearance(12.2, openings, [0],
fail_in=3.0, warn_in=6.0)
assert sev == 'FAIL' # 2.4" away
sev, _, d = ticketpage.dunnage_clearance(11.0, openings, [0],
fail_in=3.0, warn_in=6.0)
assert sev == 'FAIL' # inside
assert d == 0.0
# --------------------------------------------------------- strand debond
def test_strand_debond_runs_short_run_flags_the_crossing_strand():
"""A 22 ft piece with two openings 3.0 ft apart leaves a run shorter
than the 4.0 ft threshold. Only the strand at y=2.0 (inside both
openings' y0..y1 band) crosses that run; the strand at y=8.0 does
not and is left out."""
length_ft = 22.0
openings = [(8.0, 9.0, 0.0, 3.0), (12.0, 13.0, 0.0, 3.0)]
strands_in = [2.0 * 12.0, 8.0 * 12.0] # y=2.0 ft and y=8.0 ft, inches
runs = ticketpage.strand_debond_runs(length_ft, openings, strands_in, 4.0)
assert runs == [{'x0': 9.0, 'x1': 12.0, 'length_ft': 3.0,
'strands_in': [24.0]}]
def test_strand_debond_runs_long_run_is_clean():
"""Same shape, openings 5.0 ft apart -- the run clears the 4.0 ft
threshold, so nothing is reported even though the strand still
crosses both openings."""
length_ft = 22.0
openings = [(8.0, 9.0, 0.0, 3.0), (14.0, 15.0, 0.0, 3.0)]
strands_in = [2.0 * 12.0]
runs = ticketpage.strand_debond_runs(length_ft, openings, strands_in, 4.0)
assert runs == []
def test_strand_debond_runs_no_openings_is_empty_not_a_finding():
"""A piece with no through openings has nothing for 4.7 to say --
empty list, not a finding, and distinct from the missing-input None."""
runs = ticketpage.strand_debond_runs(22.0, [], [24.0], 4.0)
assert runs == []
def test_strand_debond_runs_missing_inputs_is_not_run_not_ok():
"""No length, no strand positions, or no threshold -- these are 'could
not judge', which this function marks with None. A caller that reads
None as an empty list (== OK) would repeat the exact bug this project
keeps finding: a check that returns fine because it never looked."""
openings = [(8.0, 9.0, 0.0, 3.0), (12.0, 13.0, 0.0, 3.0)]
assert ticketpage.strand_debond_runs(None, openings, [24.0], 4.0) is None
assert ticketpage.strand_debond_runs(0.0, openings, [24.0], 4.0) is None
assert ticketpage.strand_debond_runs(22.0, openings, [], 4.0) is None
assert ticketpage.strand_debond_runs(22.0, openings, [24.0], None) is None
assert ticketpage.strand_debond_runs(22.0, openings, [24.0], 0.0) is None
# and none of these collapse to the same shape as the clean [] result
assert ticketpage.strand_debond_runs(None, openings, [24.0], 4.0) != []
def test_strand_debond_runs_notch_only_catches_the_strand_it_crosses():
"""A notch spanning only a fifth of an 8 ft width does not need its own
hole-vs-notch threshold here: the strand at y=0.5 ft (inside the
notch's 0..1.6 ft band) is caught by every short run its path
crosses; the strand at y=6.0 ft (outside the band entirely) never
even gets a cut built at its position, no matter how short the notch
runs along the length."""
length_ft = 20.0
notch = (9.0, 9.5, 0.0, 1.6) # 1.6 of 8.0 ft width = 20%
other_gap = (13.0, 13.5, 0.0, 1.6)
openings = [notch, other_gap]
strands_in = [0.5 * 12.0, 6.0 * 12.0] # 6.0 in, 72.0 in
runs = ticketpage.strand_debond_runs(length_ft, openings, strands_in, 4.0)
assert runs # at least one short run found
all_strands = set()
for r in runs:
all_strands.update(r['strands_in'])
assert all_strands == set([6.0]) # only the in-band strand
assert 72.0 not in all_strands # the outside strand never appears
def test_strand_debond_runs_piece_end_counts_as_a_boundary():
"""One opening pinches BOTH stubs to under the threshold: the 2.0 ft
run to the start and the 3.0 ft run to the end are each measured
exactly like a run bounded by two openings would be -- the piece's
own end is a real boundary, not a reason to skip the stub."""
length_ft = 6.0
openings = [(2.0, 3.0, 0.0, 3.0)] # 2.0 ft stub, then a 3.0 ft stub
strands_in = [12.0] # y=1.0 ft, inside the opening band
runs = ticketpage.strand_debond_runs(length_ft, openings, strands_in, 4.0)
bounds = set([(r['x0'], r['x1']) for r in runs])
assert (0.0, 2.0) in bounds
assert (3.0, 6.0) in bounds
def test_openings_hit_wp199():
"""WP199: ticket dunnage lands 4.75 / 18.17; OPENING 1 runs 15.60..18.96,
so the far block sits over it (the real defect of this whole refactor)."""
stations = ticketpage.dunnage_stations([4.75], length_ft=22.9167)
hits = ticketpage.openings_hit(stations, [(15.60, 18.96)])
assert [s for s, _ in hits] == [18.1667]
# the near block at 4.75 is clear
assert all(s != 4.75 for s, _ in hits)
def test_openings_edge_is_a_graze_not_a_hit():
hits = ticketpage.openings_hit([18.96, 19.0], [(15.60, 18.96)])
assert hits == [] # exactly on the edge is not over the hole
def test_pick_stations_match_wp412():
"""WP412's four picking stations match the four modelled P136."""
printed = ticketpage.stations_from_ends(24.9167, [2.5, 7.5, 7.5, 2.5])
model = [2.5, 10.0, 14.917, 22.417]
matched, unmatched = ticketpage.pick_stations_match(printed, model)
assert len(matched) == 4
assert unmatched == []
# ------------------------------------------------------------- weight
def test_weight_three_way_clean():
"""WP219 15,525 / WP220 15,371 / WP199 24,345-vs-24,348 all clean."""
for p, m, r in ((15525, 15525, 15525),
(15371, 15371, 15371),
(24345, 24348, 24348)):
sev, code, _ = ticketpage.weight_three_way(p, m, r)
assert sev == 'OK', (p, m, r)
assert code == 'WGT-OK'
def test_weight_three_way_diagnoses_each_side():
"""Which pair agrees says which side is wrong."""
# printed stale: print differs, geometry reads clean
sev, code, msg = ticketpage.weight_three_way(20000, 15525, 15525)
assert (sev, code) == ('WARN', 'WGT-PRINTED')
assert 'stale' in msg
# model wrong: geometry differs from a solid that matches the ticket
sev, code, msg = ticketpage.weight_three_way(15525, 17500, 15525)
assert (sev, code) == ('WARN', 'WGT-MODEL')
# our read wrong: printed == measured but reconstructed differs
sev, code, msg = ticketpage.weight_three_way(15525, 15525, 14000)
assert (sev, code) == ('WARN', 'WGT-READ')
assert 'tool' in msg
def test_weight_three_way_incomplete():
sev, code, _ = ticketpage.weight_three_way(None, 15525, 15525)
assert (sev, code) == ('WARN', 'WGT-INCOMPLETE')
def test_weight_three_way_tolerance():
"""WP199 printed 24,345 vs model 24,348: 0.012% -- inside 2%."""
sev, code, _ = ticketpage.weight_three_way(24345, 24348, 24348)
assert (sev, code) == ('OK', 'WGT-OK')
def test_weight_agreement_runs_the_three_way():
"""All three present: the checklist's own diagnosis, by name."""
sev, code, msg = ticketpage.weight_agreement(15525, 15525, 15525)
assert (sev, code) == ('OK', 'WGT-OK')
# printed stale -- the model is consistent with itself
sev, code, msg = ticketpage.weight_agreement(20000, 15525, 15525)
assert (sev, code) == ('WARN', 'WGT-PRINTED')
# the solid does not match the dimensions: a model-side problem
sev, code, msg = ticketpage.weight_agreement(15525, 17500, 15525)
assert (sev, code) == ('WARN', 'WGT-MODEL')
# printed and solid agree: the model is right, our read is not
sev, code, msg = ticketpage.weight_agreement(15525, 15525, 14000)
assert (sev, code) == ('WARN', 'WGT-READ')
assert 'tool' in msg
def test_weight_agreement_two_way_says_so_and_why():
"""Two of three: still compared, never presented as a clean 2.2."""
why = {'measured': 'the panel has no unit-weight parameter'}
sev, code, msg = ticketpage.weight_agreement(
15525, None, 15525, unavailable=why)
assert (sev, code) == ('WARN', 'WGT-TWO-WAY') # agreeing is not OK
assert 'two-way' in msg
assert 'no unit-weight parameter' in msg # why the third is gone
assert 'odd one out cannot be said' in msg
assert "modelled solid's own weight" in msg # WHICH one is missing
def test_weight_agreement_two_way_disagreement_is_a_fail():
sev, code, msg = ticketpage.weight_agreement(
20000, None, 15525, unavailable={'measured': 'no solid was readable'})
assert (sev, code) == ('WARN', 'WGT-TWO-WAY')
assert 'disagree by' in msg
# and it must NOT claim to know which of the two is the wrong one
assert 'stale' not in msg
def test_weight_agreement_two_way_default_reason():
"""A caller that offers no reason still gets a sentence, not a blank."""
sev, code, msg = ticketpage.weight_agreement(15525, 15525, None)
assert (sev, code) == ('WARN', 'WGT-TWO-WAY')
assert 'could not be read' in msg
def test_weight_agreement_could_not_judge_is_none():
"""Fewer than two weights is 'could not judge', not a pass."""
assert ticketpage.weight_agreement(15525, None, None) is None
assert ticketpage.weight_agreement(None, None, None) is None
def test_weight_agreement_parses_printed_text():
"""The printed weight arrives as title-block text, commas and all."""
sev, code, _ = ticketpage.weight_agreement('15,525', 15525, 15525)
assert (sev, code) == ('OK', 'WGT-OK')
# text that is not a number is a missing source, not a crash
assert ticketpage.weight_agreement('SEE PLAN', None, 15525) is None
# --------------------- reconstruction NOT APPLICABLE (WP100, 2026-09-08) ---
#
# A stepped/tapered wythe (`liftexport._reconstructed_ft3`) is a known tool
# LIMITATION, not a missing input -- so this is a SEPARATE function from
# `weight_agreement`, and unlike its generic two-way path (deliberately
# never OK, `test_weight_agreement_two_way_says_so_and_why` above) this one
# CAN say OK, because with the reconstruction's own absence already
# explained, printed-vs-measured really is the whole comparison left.
def test_weight_agreement_no_reconstruction_ok_when_the_two_agree():
sev, code, msg = ticketpage.weight_agreement_no_reconstruction(
9045, 9115, 0.02, 'stepped or tapered wythe')
assert (sev, code) == ('OK', 'WGT-OK')
assert '9,115' in msg and '9,045' in msg
assert 'not applicable' in msg
assert 'stepped or tapered wythe' in msg
def test_weight_agreement_no_reconstruction_warns_on_real_disagreement():
sev, code, msg = ticketpage.weight_agreement_no_reconstruction(
8000, 9115, 0.02, 'stepped or tapered wythe')
assert (sev, code) == ('WARN', 'WGT-TWO-WAY-NA')
assert 'not applicable' in msg
# never claims to know which of printed/measured is the wrong one --
# that diagnosis needs the third (reconstructed) number, which is
# exactly what is missing here.
assert 'stale' not in msg
def test_weight_agreement_no_reconstruction_could_not_judge_is_none():
assert ticketpage.weight_agreement_no_reconstruction(
None, 9115, 0.02, 'reason') is None
assert ticketpage.weight_agreement_no_reconstruction(
9045, None, 0.02, 'reason') is None
def test_weight_lb_parses_what_asvaluestring_actually_returns():
"""`weight_lb` is the parser standing between a Revit AsValueString()
read (or OCR'd title-block text) and every weight comparison in this
module. Pinned hard because it is the only host-testable half of the
fix for the live 2026-09-02 finding: a custom cast-weight shared
parameter's AsDouble() returned 560537.22 while AsValueString() read
"38,409" -- a 14.6x units artifact (the parameter name is plant
knowledge and does not belong in this repository). That units artifact
never reaches here because the production read (ticketqc.rstr -> pstr
-> AsValueString) is a string, and everything downstream goes through
this function.
"""
# commas, exactly what a formatted title-block weight looks like
assert ticketpage.weight_lb('38,409') == 38409.0
assert ticketpage.weight_lb('560,537.22') == 560537.22
# plain numbers, ints, and already-float values pass through
assert ticketpage.weight_lb(15525) == 15525.0
assert ticketpage.weight_lb(15525.5) == 15525.5
# incidental whitespace
assert ticketpage.weight_lb(' 15,525 ') == 15525.0
# not a number at all -- "could not judge", never a crash and never 0
assert ticketpage.weight_lb('SEE PLAN') is None
assert ticketpage.weight_lb('') is None
assert ticketpage.weight_lb(None) is None
def test_weight_param_note_only_speaks_when_it_disagrees():
"""The stored weight parameter is not one of the three; it is reported
only when it contradicts a derived weight (it is what the handling
gate reads)."""
assert ticketpage.weight_param_note(15525, 15525, 15525) is None
assert ticketpage.weight_param_note(None, 15525, 15525) is None
sev, code, msg = ticketpage.weight_param_note(13100, 15525, 15525)
assert (sev, code) == ('WARN', 'WGT-PARAM')
assert 'modelled solid' in msg
# with no solid read, it falls back to the ticket and says so
sev, code, msg = ticketpage.weight_param_note(13100, None, 15525)
assert (sev, code) == ('WARN', 'WGT-PARAM')
assert 'the ticket' in msg
def test_checklist_two_way_weight_is_visible_as_warn():
"""A two-way result must reach the checklist table as WARN with the
reason attached -- an OK row with no note is the silent degradation."""
from vibe import ticketpage as TP
sev, code, msg = TP.weight_agreement(
15525, None, 15525, unavailable={'measured': 'no solid was readable'})
rows = TP.checklist_states(
[_F(sev, code, msg)],
log=[('check_weight_three_way', 'RAN', None)])
num, name, grade, status, note = _row(rows, '2.2')
assert status == 'WARN'
assert note and 'two-way' in note
# ------------------------------------------------------------- invisible
def test_clean_text_strips_zero_width():
"""U+200E rode in a model parameter's ValueString all the way into
evidence json and crashed a tool that printed it (measured)."""
from vibe import ticketpage as TP
assert TP.clean_text(u'x\u200Ey') == 'xy'
assert TP.clean_text(u'\u200e') == '' # nothing but the mark
assert TP.clean_text(u'a\u200fb\u00adc\ufeff') == 'abc'
assert TP.clean_text('plain') == 'plain'
assert TP.clean_text(None) is None
# -------------------------------------------- Phase 5 checklist table
class _F(object):
"""A finding stub: only what checklist_states reads."""
def __init__(self, sev, code, msg=''):
self.severity = sev
self.code = code
self.message = msg
def _row(table, num):
for row in table:
if row[0] == num:
return row
raise AssertionError('no row {0} in table'.format(num))
def test_checklist_ran_empty_is_ok_not_notrun():
"""Claude's self-test, automated: an owner check that RAN but emitted
no finding must show OK -- and stay OK when its OK finding is removed
(the 'silence looks like fine' trap is exactly what this table kills)."""
from vibe import ticketpage as TP
# 6.1 owned by check_lifter_capacity: RAN, no finding at all
rows = TP.checklist_states([], log=[('check_lifter_capacity',
'RAN', None)])
assert _row(rows, '6.1')[3] == 'OK'
# with an OK finding present, still OK
rows2 = TP.checklist_states([_F('OK', 'LIFT-CAPACITY')],
log=[('check_lifter_capacity', 'RAN', None)])
assert _row(rows2, '6.1')[3] == 'OK'
def test_checklist_skipped_must_carry_reason():
"""A SKIPPED owner check yields NOT RUN with a non-empty reason -- the
check's own words, never a guessed one."""
from vibe import ticketpage as TP
rows = TP.checklist_states(
[],
log=[('check_weight_three_way', 'SKIPPED',
'plant has no sheet.weight mapping')])
num, name, grade, status, note = _row(rows, '2.2')
assert status == 'NOT RUN'
assert note and 'sheet.weight' in note
def test_checklist_no_code_not_confused_with_skipped():
"""An item with no owner check at all must NOT pick up some other
check's skip reason.
Written against 6.5, then 5.3, and now 2.1 -- both of the earlier ones
acquired a disposition of their own (6.5 a check on 2026-08-30, 5.3 the
FOR ENGINEER state it always should have had). The property is about ANY
unowned item, so it keeps moving to one that still is.
"""
from vibe import ticketpage as TP
rows = TP.checklist_states(
[],
log=[('check_weight_three_way', 'SKIPPED',
'plant has no sheet.weight mapping')])
_2_1 = _row(rows, '2.1')
assert _2_1[3] == 'NOT RUN'
assert 'sheet.weight' not in (_2_1[4] or '')
_2_2 = _row(rows, '2.2')
assert _2_2[3] == 'NOT RUN'
assert 'sheet.weight' in _2_2[4]
# ------------------------------- 5.3: a person's row, not an unrun one
#
# The defect the owner reported as "5.3 没跑" (2026-08-30), pinned from both
# ends: the row must read FOR ENGINEER, and the rows that genuinely have not
# run must NOT be swept into FOR ENGINEER along with it.
def test_53_is_for_engineer_and_never_the_red_not_run_it_used_to_print():
"""Grade C, no check function, and a reason recorded: a person's row.
It printed NOT RUN -- red, the same colour as FAIL -- on every ticket
because `CHECKLIST_FOR_ENGINEER` was a hand-kept tuple holding only 5.5.
"""
from vibe import ticketpage as TP
row = _row(TP.checklist_states([], log=[]), '5.3')
assert row[2] == 'C'
assert row[3] == 'FOR ENGINEER'
assert TP.state_color(row[3]) == 'yellow'
assert TP.state_color(row[3]) != TP.state_color('FAIL')
def test_53_says_in_its_own_words_why_nothing_was_computed():
"""A FOR ENGINEER row carries the item's OWN reason, not a stock
sentence about grade D -- 5.3 is grade C and would have been lied to."""
from vibe import ticketpage as TP
rows = dict((r[0], r) for r in TP.checklist_states([], log=[]))
assert 'grade D' not in rows['5.3'][4]
assert 'grade C' in rows['5.3'][4]
assert 'person' in rows['5.3'][4]
assert 'grade D' in rows['5.5'][4]
# each row's note is the reason recorded in the table, one source
by_num = dict((i[0], i) for i in TP.CHECKLIST_ITEMS)
for num in ('5.3', '5.5'):
assert rows[num][4] == by_num[num][4]
def test_for_engineer_is_derived_from_the_table_not_kept_by_hand():
"""No check, no owner code, and a reason written down: those three are
what "a person owns this ON PURPOSE" looks like. A row missing any of
them is an unbuilt row and stays NOT RUN."""
from vibe import ticketpage as TP
assert TP.CHECKLIST_FOR_ENGINEER == ('5.3', '5.5')
for item in TP.CHECKLIST_ITEMS:
num, _name, _grade, owners, reason, check_name = item
if num in TP.CHECKLIST_FOR_ENGINEER:
assert check_name is None, num
assert not owners, num
assert reason, num
def test_an_unbuilt_row_is_not_swept_into_for_engineer_by_the_derivation():
"""1.2 and 2.1 have no check function either. They have owner codes, so
something else claims them, and with nothing claimed they are NOT RUN --
red -- exactly as before. Turning a red row yellow is the same failure
as turning it green, one shade quieter."""
from vibe import ticketpage as TP
rows = dict((r[0], r) for r in TP.checklist_states([], log=[]))
for num in ('1.2', '2.1', '2.3', '4.1', '4.2'):
assert rows[num][3] == 'NOT RUN', num
assert TP.state_color(rows[num][3]) == 'red', num
# ------------------------------------------------ ticket-set uniqueness (1.3)
def test_ticket_set_ok_pair():
from vibe import ticketpage as TP
sev, msg = TP.ticket_set_status(
['WP199 1 OF 2', 'WP199 2 OF 2'])
assert sev == 'OK'
assert '2-sheet' in msg
def test_ticket_set_ok_single():
from vibe import ticketpage as TP
sev, msg = TP.ticket_set_status(['WP210 1 OF 1'])
assert sev == 'OK'
def test_ticket_set_orphan_fails():
from vibe import ticketpage as TP
sev, msg = TP.ticket_set_status([])
assert sev == 'FAIL'
assert 'nobody drew' in msg
def test_ticket_set_missing_page_warns():
from vibe import ticketpage as TP
sev, msg = TP.ticket_set_status(['WP199 1 OF 2']) # 2 OF 2 absent
assert sev == 'WARN'
assert '2' in msg
def test_ticket_set_duplicate_warns():
from vibe import ticketpage as TP
sev, msg = TP.ticket_set_status(
['WP199 1 OF 2', 'WP199 2 OF 2', 'WP199 1 OF 2'])
assert sev == 'WARN'
assert 'duplicate' in msg
def test_ticket_set_refuses_names_only():
"""Claude's negative test: names without page markers contain no
criterion -- the check must NOT say OK. The old bug (SheetNumber read
as a parameter) returned OK on exactly this input while seeing
nothing; this is the test that would have caught it."""
from vibe import ticketpage as TP
sev, msg = TP.ticket_set_status(['WP133', 'WP133'])
assert sev != 'OK'
assert 'page marker' in msg
# -------------------------------------------------- sheet name vs mark (1.1)
def test_sheet_name_matches_ok():
from vibe import ticketpage as TP
sev, msg = TP.sheet_name_matches('WP133', ['WP133', 'WP133'])
assert sev == 'OK'
assert '2 sheet(s)' in msg
def test_sheet_name_matches_normalized():
from vibe import ticketpage as TP
sev, msg = TP.sheet_name_matches('wp133', ['WP133 ', 'WP133'])
assert sev == 'OK'
def test_sheet_name_matches_mismatch():
from vibe import ticketpage as TP
sev, msg = TP.sheet_name_matches('WP133', ['WP134', 'WP133'])
assert sev == 'WARN'
assert 'WP134' in msg
def test_sheet_name_matches_no_sheets():
from vibe import ticketpage as TP
sev, msg = TP.sheet_name_matches('WP133', [])
assert sev == 'FAIL'
# -------------------------------------------------- invisible chars (8.2)
def test_invisible_chars_ok():
from vibe import ticketpage as TP
sev, msg = TP.invisible_chars(['WP133 1 OF 2', 'plain'])
assert sev == 'OK'
def test_invisible_chars_detects():
from vibe import ticketpage as TP
sev, msg = TP.invisible_chars([u'x\u200ey', 'plain'])
assert sev == 'WARN'
assert '0x200e' in msg
def test_invisible_chars_none_skipped():
from vibe import ticketpage as TP
sev, msg = TP.invisible_chars([None, ''])
assert sev == 'OK'
# --------------------------------------------------- wwf designation (5.4)
def test_wwf_designation_matches():
from vibe import ticketpage as TP
sev, msg = TP.wwf_designation_status('6X6-W1.4XW1.4',
['MESH 6X6-W1.4XW1.4 (SCHEDULED)'])
assert sev == 'OK'
assert 'modelled' in msg
def test_wwf_designation_mismatch():
from vibe import ticketpage as TP
sev, msg = TP.wwf_designation_status('6X6-W1.4XW1.4', ['MESH 4X4'])
assert sev == 'WARN'
assert '4X4' in msg
def test_wwf_designation_no_design_side():
"""The branch this check actually walks on this model: spec has no
wwf -> NOT RUN with the pointer to Extract Design (probe side, but
the pure core must not invent a verdict either)."""
from vibe import ticketpage as TP
sev, msg = TP.wwf_designation_status('', ['MESH 6X6'])
assert sev == 'NOT RUN'
# ------------------------------------------- 1.4 view name vs design
TOKEN_RE = r'([A-Za-z]+)\s*(\d+)'
def test_view_name_design_catches_the_measured_case():
"""The finding this check was written for: a view named for WP2 placed
on a WP1 ticket (measured on 100 sheets, 2026-08-27)."""
from vibe import ticketpage as TP
sev, msg = TP.view_name_design_status(
'WP1', ['Elevation Top', 'Elevation Front',
'TICKET NOTE - WP2- STRAND JACKING FORCE'], TOKEN_RE)
assert sev == 'WARN'
assert 'WP2' in msg
# never FAIL, and it must say the name is the finding -- the audit
# proved the note's CONTENT was right.
assert 'do not infer' in msg
def test_view_name_design_agreeing_label_is_ok():
from vibe import ticketpage as TP
sev, msg = TP.view_name_design_status(
'WP1', ['TICKET NOTE - WP1 - STRAND JACKING FORCE'], TOKEN_RE)
assert sev == 'OK'
def test_view_name_design_ignores_unlabelled_names():
"""'Elevation Top 2' reduces to TOP2 under the same pattern. A check
that reported it would fire on ordinary view names on every ticket;
only a token with THIS design's letter prefix is compared."""
from vibe import ticketpage as TP
sev, msg = TP.view_name_design_status(
'WP1', ['Elevation Top 2', 'Section A-A', 'Detail 3'], TOKEN_RE)
assert sev == 'OK'
assert 'no placed view labels itself' in msg
def test_view_name_design_reduces_page_suffixes():
"""PNL1.0 and PNL1 are one design; a view labelled PNL1.0 on a PNL1
ticket is not a finding."""
from vibe import ticketpage as TP
sev, _ = TP.view_name_design_status(
'PNL1', ['NOTE - PNL1.0 - DUNNAGE'], TOKEN_RE)
assert sev == 'OK'
def test_view_name_design_could_not_judge_is_none_not_ok():
"""No design, no pattern, or no views -> None (the caller reports NOT
RUN). Returning OK here is the silent-pass failure the tool exists to
prevent."""
from vibe import ticketpage as TP
assert TP.view_name_design_status('', ['WP2 NOTE'], TOKEN_RE) is None
assert TP.view_name_design_status('WP1', ['WP2 NOTE'], None) is None
assert TP.view_name_design_status('WP1', [], TOKEN_RE) is None
assert TP.view_name_design_status('WP1', ['WP2 NOTE'], '([') is None
def test_view_name_design_reports_every_offender():
"""A number alone is not information -- the message must name each one."""
from vibe import ticketpage as TP
sev, msg = TP.view_name_design_status(
'WP1', ['NOTE - WP2 - A', 'NOTE - WP3 - B', 'Elevation Top'],
TOKEN_RE)
assert sev == 'WARN'
assert 'WP2' in msg and 'WP3' in msg
# ------------------------------------------- 6.5 note views say something
def _view(name, placed=True, texts=(), other=0):
return {'name': name, 'placed': placed, 'texts': list(texts),
'other_elements': other}
def test_note_view_heading_over_nothing_is_the_finding():
"""The measured shape: the view contains its own title and nothing
else, which prints as a heading over blank paper."""
from vibe import ticketpage as TP
sev, msg = TP.note_view_content_status([
_view('DUNNAGE', texts=['DUNNAGE']),
_view('ERECTION', texts=['ERECTION']),
_view('PICKING-STRIPPING', texts=['PICKING/STRIPPING']),
])
assert sev == 'WARN'
for n in ('DUNNAGE', 'ERECTION', 'PICKING-STRIPPING'):
assert n in msg
def test_note_view_with_an_instruction_is_ok():
from vibe import ticketpage as TP
sev, msg = TP.note_view_content_status([
_view('DUNNAGE', texts=['DUNNAGE', 'STACK ON 4X4 AT 1/5 POINTS']),
])
assert sev == 'OK'
def test_note_view_drawn_instruction_counts_as_content():
"""If the instruction is DRAWN rather than written, text notes alone
report the view as empty and the check fails every ticket at that
plant. View-owned graphics count."""
from vibe import ticketpage as TP
sev, msg = TP.note_view_content_status([
_view('ERECTION', texts=['ERECTION'], other=7),
])
assert sev == 'OK'
assert 'graphics' in msg
def test_note_view_not_placed_is_reported_not_passed():
from vibe import ticketpage as TP
sev, msg = TP.note_view_content_status([
_view('DUNNAGE', placed=False),
_view('ERECTION', texts=['ERECTION', 'SEE ERECTION DRAWINGS']),
])
assert sev == 'WARN'
assert 'not placed' in msg and 'DUNNAGE' in msg
def test_note_view_heading_punctuation_does_not_count_as_content():
"""'PICKING/STRIPPING' in a view named 'PICKING-STRIPPING' is the same
heading; punctuation must not make an empty view look full."""
from vibe import ticketpage as TP
sev, _ = TP.note_view_content_status([
_view('PICKING-STRIPPING', texts=['PICKING / STRIPPING:']),
])
assert sev == 'WARN'
def test_note_view_blank_text_is_not_content():
from vibe import ticketpage as TP
sev, _ = TP.note_view_content_status([
_view('DUNNAGE', texts=['DUNNAGE', ' ']),
])
assert sev == 'WARN'
def test_note_view_nothing_declared_is_none_not_ok():
"""No declared note views -> could not judge, never a pass."""
from vibe import ticketpage as TP
assert TP.note_view_content_status([]) is None
assert TP.note_view_content_status(None) is None
# --------------------------------- both items now occupy a checklist row
def test_checklist_carries_rows_for_14_and_65():
"""Before this, 1.4 had no row at all -- it could not even report NOT
RUN, and an item nobody checked looked exactly like one that passed."""
from vibe import ticketpage as TP
nums = [row[0] for row in TP.CHECKLIST_ITEMS]
assert '1.4' in nums and '6.5' in nums
by_num = dict((r[0], r) for r in TP.CHECKLIST_ITEMS)
assert by_num['1.4'][5] == 'check_view_name_design'
assert by_num['6.5'][5] == 'check_note_views'
# re-graded 2026-08-30: both need a plant fact written down first
assert by_num['1.4'][2] == 'B'
assert by_num['6.5'][2] == 'B'
def test_checklist_14_and_65_report_not_run_when_the_check_skipped():
from vibe import ticketpage as TP
log = [('check_view_name_design', 'SKIPPED', 'no design token pattern'),
('check_note_views', 'SKIPPED', 'no notes.instruction_views')]
rows = dict((r[0], r) for r in TP.checklist_states([], log))
assert rows['1.4'][3] == 'NOT RUN'
assert 'design token' in rows['1.4'][4]
assert rows['6.5'][3] == 'NOT RUN'
assert 'instruction_views' in rows['6.5'][4]
# ------------- 6.5 groups of alternatives, and product families (08-30)
#
# Placeholder names throughout: the real ones are plant knowledge and live
# in the plant file, which is not in this repository.
PANEL_SETS = {
'PANEL': {
'DUNNAGE': ['NOTE - PANEL - DUNNAGE'],
'ERECTION': ['NOTE - PANEL - ERECTION',
'NOTE - PANEL - ERECTION (V2)',
'NOTE - PANEL - PICKING/ERECTION'],
'PICKING': ['NOTE - PANEL - PICKING',
'NOTE - PANEL - PICKING (V3)',
'NOTE - PANEL - PICKING/ERECTION'],
},
'SLAB': {
'DUNNAGE': ['NOTE - SLAB - DUNNAGE'],
'PICKING': ['NOTE - SLAB - PICKING'],
},
}
def _labels(groups):
return sorted(g['label'] for g in groups)
def test_a_flat_list_of_names_still_means_one_group_each():
"""Backward compatibility is not academic -- PLANT_STARTER and the
existing tests use this shape."""
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_views': ['DUNNAGE', 'ERECTION']}, ['DUNNAGE'])
assert reason is None
assert _labels(groups) == ['DUNNAGE', 'ERECTION']
assert [g['placed'] for g in groups] == [['DUNNAGE'], []]
def test_one_variant_satisfies_its_whole_group():
"""The measured case: 15 sheets carry the V3 picking note and the rest
carry the plain one. A flat list would have called all 15 defective."""
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_view_sets': PANEL_SETS},
['NOTE - PANEL - DUNNAGE', 'NOTE - PANEL - ERECTION',
'NOTE - PANEL - PICKING (V3)'])
assert reason is None
assert _labels(groups) == ['DUNNAGE', 'ERECTION', 'PICKING']
assert all(g['placed'] for g in groups)
def test_a_group_with_none_of_its_variants_names_the_group_once():
"""Listing four picking variants as four missing views would be four
times the noise for one fact."""
from vibe import ticketpage as TP
groups, _ = TP.instruction_view_groups(
{'instruction_view_sets': PANEL_SETS},
['NOTE - PANEL - DUNNAGE', 'NOTE - PANEL - ERECTION'])
missing = [g['label'] for g in groups if not g['placed']]
assert missing == ['PICKING']
sev, msg = TP.note_view_content_status(
[{'name': 'PICKING', 'placed': False, 'texts': [],
'other_elements': 0}])
assert sev == 'WARN'
assert msg.count('PICKING') == 1
assert 'V3' not in msg
def test_a_panel_ticket_is_never_asked_for_the_slab_views():
"""A slab ticket legitimately has no erection note and a panel ticket
has no slab views at all. Only the family the ticket carries counts."""
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_view_sets': PANEL_SETS},
['NOTE - PANEL - DUNNAGE', 'NOTE - PANEL - ERECTION',
'NOTE - PANEL - PICKING'])
assert reason is None
assert set(g['set'] for g in groups) == set(['PANEL'])
slab, _ = TP.instruction_view_groups(
{'instruction_view_sets': PANEL_SETS},
['NOTE - SLAB - DUNNAGE', 'NOTE - SLAB - PICKING'])
assert set(g['set'] for g in slab) == set(['SLAB'])
assert _labels(slab) == ['DUNNAGE', 'PICKING']
def test_an_undeclared_product_type_is_not_run_not_a_finding():
"""A beam ticket, with no beam family written down yet. Reporting its
three panel groups 'not placed' would be three inventions."""
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_view_sets': PANEL_SETS},
['NOTE - BEAM - DUNNAGE', 'NOTE - BEAM - PICKING 2PT'])
assert groups == []
assert reason and 'not declared' in reason
assert 'PANEL' in reason and 'SLAB' in reason
def test_nothing_declared_at_all_says_so():
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups({}, ['ANYTHING'])
assert groups == []
assert 'instruction_views' in reason
def test_a_combined_view_answers_both_groups():
"""One drawing that gives both instructions satisfies both."""
from vibe import ticketpage as TP
groups, _ = TP.instruction_view_groups(
{'instruction_view_sets': PANEL_SETS},
['NOTE - PANEL - DUNNAGE', 'NOTE - PANEL - PICKING/ERECTION'])
assert all(g['placed'] for g in groups)
def test_group_shapes_all_normalise_the_same_way():
from vibe import ticketpage as TP
placed = ['A', 'B2']
for decl in (
[{'ONE': 'A'}, {'TWO': ['B1', 'B2']}],
{'ONE': 'A', 'TWO': ['B1', 'B2']},
[['A'], {'TWO': ['B1', 'B2']}],
):
groups, reason = TP.instruction_view_groups(
{'instruction_views': decl}, placed)
assert reason is None
assert len(groups) == 2
assert all(g['placed'] for g in groups)
# a bare list of alternatives is named by its first name
groups, _ = TP.instruction_view_groups(
{'instruction_views': [['B1', 'B2']]}, placed)
assert groups[0]['label'] == 'B1'
def test_view_names_match_on_case_and_spacing_only():
from vibe import ticketpage as TP
groups, _ = TP.instruction_view_groups(
{'instruction_views': ['note - panel - dunnage']},
['NOTE - PANEL - DUNNAGE'])
assert groups[0]['placed']
def test_an_empty_variant_is_reported_by_the_name_a_reader_must_open():
"""Told 'PICKING is empty' with four variants declared, a reader has no
way to know which view to open."""
from vibe import ticketpage as TP
sev, msg = TP.note_view_content_status([
{'name': 'PICKING', 'matched': 'NOTE - PANEL - PICKING (V3)',
'placed': True, 'texts': ['PICKING'], 'other_elements': 0}])
assert sev == 'WARN'
assert 'NOTE - PANEL - PICKING (V3)' in msg
def test_the_matched_variant_is_what_a_heading_is_compared_against():
"""The variant's own name is the heading it prints, punctuation and
all -- so an empty variant is still caught as empty."""
from vibe import ticketpage as TP
sev, _ = TP.note_view_content_status([
{'name': 'PICKING', 'matched': 'NOTE - PANEL - PICKING/STRIPPING (V3)',
'placed': True, 'texts': ['PICKING / STRIPPING:'],
'other_elements': 0}])
assert sev == 'WARN'
sev, _ = TP.note_view_content_status([
{'name': 'PICKING', 'matched': 'NOTE - PANEL - PICKING/STRIPPING (V3)',
'placed': True,
'texts': ['PICKING/STRIPPING', 'PICK AT 1/5 POINTS'],
'other_elements': 0}])
assert sev == 'OK'
def test_an_always_required_flat_list_survives_alongside_sets():
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_views': ['NOTE - ALWAYS'],
'instruction_view_sets': PANEL_SETS},
['NOTE - PANEL - DUNNAGE'])
assert reason is None
assert 'NOTE - ALWAYS' in _labels(groups)
assert [g for g in groups if g['label'] == 'NOTE - ALWAYS'][0]['set'] is None
def test_a_view_answering_two_groups_is_named_once():
from vibe import ticketpage as TP
row = lambda label: {'name': label, 'matched': 'NOTE - PICK/ERECT',
'placed': True, 'texts': ['NOTE', 'DO THIS'],
'other_elements': 0}
sev, msg = TP.note_view_content_status([row('ERECTION'), row('PICKING')])
assert sev == 'OK'
assert msg.count('NOTE - PICK/ERECT') == 1
# ----------------------------- 2.4 and 4.5 now occupy a checklist row (08-30)
def test_checklist_carries_rows_for_24_and_45():
"""2.4 and 4.5 had no row at all until 2026-08-30 -- the report did not
mention them, so a reader saw a clean table and concluded all items were
checked. Both are now BUILT and own a check, so the row must come from
that check's own disposition: silence with no log is still NOT RUN, and
a check that ran and found nothing is OK."""
from vibe import ticketpage as TP
by_num = dict((r[0], r) for r in TP.CHECKLIST_ITEMS)
for num in ('2.4', '4.5', '5.3'):
assert num in by_num, num
assert by_num['2.4'][5] == 'check_cast_extras'
assert by_num['2.4'][3] == ('CAST-EXTRA',)
assert by_num['4.5'][5] == 'check_debond_callout'
assert by_num['4.5'][3] == ('STRAND-CALLOUT',)
# No log at all: neither may claim a pass. (Same as every other built
# row -- 1.1 included -- now that these two carry a check instead of a
# standing "not implemented" sentence.)
rows = dict((r[0], r) for r in TP.checklist_states([], []))
for num in ('1.1', '2.4', '4.5'):
assert rows[num][3] == 'NOT RUN', num
# The check said why it could not run: that reason is the row's.
log = [('check_cast_extras', 'SKIPPED', 'no families.cast_include'),
('check_debond_callout', 'SKIPPED', '4.7 could not establish it')]
rows = dict((r[0], r) for r in TP.checklist_states([], log))
assert rows['2.4'][3] == 'NOT RUN'
assert 'cast_include' in rows['2.4'][4]
assert rows['4.5'][3] == 'NOT RUN'
assert '4.7' in rows['4.5'][4]
def test_checklist_numbers_are_unique():
from vibe import ticketpage as TP
nums = [r[0] for r in TP.CHECKLIST_ITEMS]
assert len(nums) == len(set(nums))
# ------------------------------------- BED-OFFSET reads left_rail_is_fixed
def test_bed_offset_false_rail_spends_the_slack():
from vibe import ticketpage as TP
# 144" bed, 143" piece -> 1" of slide is allowed.
assert TP.bed_offset_status(0.5, 143.0, 144.0, False)[0] == 'OK'
sev, code, msg = TP.bed_offset_status(2.0, 143.0, 144.0, False)
assert sev == 'FAIL' and code == 'BED-OFFSET'
assert 'can only move 1"' in msg
def test_bed_offset_true_rail_is_never_silently_the_false_behaviour():
"""A fixed left rail: the pattern cannot slide, and nobody has recorded
what it may do instead. Report the offset, do not judge it, and never
return the OK that the false path would have produced."""
from vibe import ticketpage as TP
sev, code, msg = TP.bed_offset_status(0.5, 143.0, 144.0, True)
assert sev == 'WARN' and code == 'BED-OFFSET'
assert 'not judged' in msg and 'FIXED' in msg
# even a zero offset is not an OK on a true plant
assert TP.bed_offset_status(0.0, 143.0, 144.0, True)[0] == 'WARN'
def test_bed_offset_unrecorded_flag_says_so_in_the_message():
from vibe import ticketpage as TP
sev, _c, msg = TP.bed_offset_status(0.5, 143.0, 144.0, None)
assert sev == 'OK' and 'unrecorded' in msg
def test_bed_offset_piece_wider_than_bed_beats_every_flag():
from vibe import ticketpage as TP
for flag in (True, False, None):
sev, code, _m = TP.bed_offset_status(0.0, 150.0, 144.0, flag)
assert (sev, code) == ('FAIL', 'BED-WIDTH')
def test_bed_offset_nothing_to_judge_returns_none():
from vibe import ticketpage as TP
assert TP.bed_offset_status(0.5, None, 144.0, False) is None
assert TP.bed_offset_status(0.5, 143.0, None, False) is None
# --------------------------------------------- optional note-view groups
#
# A note the manufacturer's standard itself calls CONDITIONAL. Declared as
# an ordinary group it would report 'not placed' on every ticket that does
# not need it -- a permanent false alarm, which is the exact failure the
# group mechanism was built to avoid.
OPTIONAL_SETS = {
'SLAB': {
'DUNNAGE': ['NOTE - SLAB - DUNNAGE'],
'PICKING': ['NOTE - SLAB - PICKING'],
'ERECTION': {'optional': True, 'views': ['NOTE - SLAB - ERECTION']},
},
}
def test_an_absent_optional_group_is_silence_not_a_finding():
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_view_sets': OPTIONAL_SETS},
['NOTE - SLAB - DUNNAGE', 'NOTE - SLAB - PICKING'])
assert reason is None
assert _labels(groups) == ['DUNNAGE', 'PICKING']
assert all(g['placed'] for g in groups)
def test_a_placed_optional_group_is_judged_like_any_other():
from vibe import ticketpage as TP
groups, reason = TP.instruction_view_groups(
{'instruction_view_sets': OPTIONAL_SETS},
['NOTE - SLAB - DUNNAGE', 'NOTE - SLAB - PICKING',
'NOTE - SLAB - ERECTION'])
assert reason is None
assert _labels(groups) == ['DUNNAGE', 'ERECTION', 'PICKING']
erection = [g for g in groups if g['label'] == 'ERECTION'][0]
assert erection['placed'] == ['NOTE - SLAB - ERECTION']
# placed but printing only its own heading is still the finding
sev, msg = TP.note_view_content_status([
{'name': 'ERECTION', 'matched': 'NOTE - SLAB - ERECTION',
'placed': True, 'texts': ['ERECTION'], 'other_elements': 0}])
assert sev == 'WARN'
assert 'heading' in msg
def test_an_optional_group_still_identifies_the_product_family():
"""Carrying a family's conditional note is as good a sign of the
family as any other, so it counts when picking the set."""
from vibe import ticketpage as TP
sets = {'PANEL': PANEL_SETS['PANEL'], 'SLAB': OPTIONAL_SETS['SLAB']}
groups, reason = TP.instruction_view_groups(
{'instruction_view_sets': sets}, ['NOTE - SLAB - ERECTION'])
assert reason is None
assert set(g['set'] for g in groups) == set(['SLAB'])
def test_optional_defaults_to_false_for_every_older_shape():
"""Nothing that was required stops being required."""
from vibe import ticketpage as TP
for decl in (['A'], [['A', 'B']], {'ONE': ['A']}, [{'ONE': 'A'}]):
groups = TP._normalise_groups(decl)
assert groups and all(g['optional'] is False for g in groups)
groups = TP._normalise_groups({'ONE': {'optional': True, 'views': ['A']}})
assert groups[0]['optional'] is True
assert groups[0]['alternatives'] == ['A']
def test_a_map_of_groups_is_not_mistaken_for_an_option_map():
"""A map without a views key is a map OF groups, as it always was."""
from vibe import ticketpage as TP
groups = TP._normalise_groups({'ONE': ['A'], 'TWO': ['B']})
assert _labels(groups) == ['ONE', 'TWO']
def test_the_views_key_accepts_its_aliases_and_a_bare_string():
from vibe import ticketpage as TP
for key in ('views', 'alternatives', 'names', 'any_of'):
groups = TP._normalise_groups({'ONE': {'optional': True, key: 'A'}})
assert groups[0]['alternatives'] == ['A']
assert groups[0]['optional'] is True
# ------------------------- 5.2: the enumeration, not the residual (A-23)
#
# The standard places reinforcing by class of bar, positively. These tests
# pin the two cases the old residual got wrong (a full-length bar and a
# full-width transverse bar are drawn in a SECTION, never on the plan) and
# the cases where the tool must say it could not classify rather than fall
# back to flagging.
def test_full_length_longitudinal_bar_belongs_in_section_a_not_the_plan():
from vibe import ticketpage as TP
home, why = TP.bar_drawing_home('length', 330.0, 331.5, 0.9)
assert home == TP.BAR_HOME_SECTION_A
assert 'note 25' in why
def test_partial_length_bar_belongs_on_the_plan():
from vibe import ticketpage as TP
home, why = TP.bar_drawing_home('length', 48.0, 331.5, 0.9)
assert home == TP.BAR_HOME_PLAN
assert 'plan' in why
def test_full_width_transverse_bar_belongs_in_section_b_not_the_plan():
from vibe import ticketpage as TP
home, why = TP.bar_drawing_home('width', 137.0, 138.0, 0.9)
assert home == TP.BAR_HOME_SECTION_B
assert 'note 40' in why
def test_part_width_transverse_bar_is_unplaced_by_the_standard():
"""Note 26 only keeps transverse bars out of A-1; note 40 covers the
full-width one. Nothing says where a part-width one goes, so the tool
says it could not classify it -- it does not fall back to the plan."""
from vibe import ticketpage as TP
home, why = TP.bar_drawing_home('width', 24.0, 138.0, 0.9)
assert home is None
assert 'does not say' in why
def test_no_full_length_fraction_stands_the_classification_down():
"""The threshold is plant knowledge with a null default, exactly like
rails.max_bed_width_in. Unset, nothing is classified and nothing is
guessed."""
from vibe import ticketpage as TP
for axis, reach in (('length', 330.0), ('width', 137.0)):
home, why = TP.bar_drawing_home(axis, reach, 331.5, None)
assert home is None
assert 'full_length_bar_fraction' in why
def test_a_nonsense_fraction_is_refused_rather_than_used():
from vibe import ticketpage as TP
for bad in (0, -0.5, 1.5, 'x'):
assert TP.bar_drawing_home('length', 330.0, 331.5, bad)[0] is None
def test_unusable_extents_are_could_not_classify_never_a_home():
from vibe import ticketpage as TP
assert TP.bar_drawing_home('length', None, 331.5, 0.9)[0] is None
assert TP.bar_drawing_home('length', 330.0, 0, 0.9)[0] is None
assert TP.bar_drawing_home(None, 330.0, 331.5, 0.9)[0] is None
# every non-classification carries a reason, never a bare None
assert TP.bar_drawing_home(None, 330.0, 331.5, 0.9)[1]
def test_bar_run_axis_reads_the_dominant_in_plane_extent():
from vibe import ticketpage as TP
# axes: length=0, width=2, thickness=1 (a panel lying in the form)
assert TP.bar_run_axis([300.0, 0.5, 0.5], 0, 2, 1) == 'length'
assert TP.bar_run_axis([0.5, 0.5, 130.0], 0, 2, 1) == 'width'
def test_bar_run_axis_refuses_the_cases_it_cannot_read():
from vibe import ticketpage as TP
# stands through the thickness: neither longitudinal nor transverse
assert TP.bar_run_axis([0.5, 8.0, 0.5], 0, 2, 1) is None
# square in plan (a hoop's box): no run direction to read
assert TP.bar_run_axis([12.0, 0.5, 12.0], 0, 2, 1) is None
# unusable input
assert TP.bar_run_axis([300.0, 0.5], 0, 2, 1) is None
assert TP.bar_run_axis([300.0, 0.5, 0.5], 0, 0, 1) is None
assert TP.bar_run_axis(None, 0, 2, 1) is None
# ------------------------------------ BOM disposition (LGD-MISSING retired)
#
# `LGD-MISSING` used to be the only thing that said anything about a ticket
# with no bill of materials. It was an orphan (no checklist item owns LGD-*),
# it called an unreadable schedule a FAIL, and its premise -- that an empty
# BOM description is a defect -- is contradicted by section 8000 of the
# manufacturer's manual. These tests pin its replacement: every check that
# wants the BOM asks bom_state and says its own honest NOT RUN, and "absent"
# never reads the same as "unreadable".
def test_a_bom_with_content_is_ok_and_needs_no_excuse():
state, reason = ticketpage.bom_state(
{'parts': [['2', 'P106', '']], 'strand_linear_ft': 249,
'schedules': ['PARTS_LIST'], 'unreadable': []})
assert state == ticketpage.BOM_OK
assert reason is None
assert ticketpage.bom_not_run_reason(
{'parts': [['2', 'P106', '']], 'schedules': ['PARTS_LIST']},
'anything') is None
def test_a_strand_row_alone_still_counts_as_a_readable_bom():
"""strand_linear_ft of 0 is a value, not an absence."""
state, _ = ticketpage.bom_state(
{'parts': [], 'strand_linear_ft': 0, 'schedules': ['STRAND_SCHEDULE'],
'unreadable': []})
assert state == ticketpage.BOM_OK
def test_no_schedule_at_all_is_absent_not_unreadable():
state, reason = ticketpage.bom_state(
{'parts': [], 'strand_linear_ft': None, 'schedules': [],
'unreadable': []})
assert state == ticketpage.BOM_ABSENT
assert 'no schedule at all' in reason
assert 'could not be read' not in reason
def test_schedules_read_but_none_is_a_bom_is_still_absent():
state, reason = ticketpage.bom_state(
{'parts': [], 'strand_linear_ft': None,
'schedules': ['SHEET INDEX'], 'unreadable': []})
assert state == ticketpage.BOM_ABSENT
assert 'SHEET INDEX' in reason
assert 'no BOM' in reason
def test_an_unreadable_schedule_never_reads_as_an_absent_bom():
"""The two states must not collapse into one -- this is the whole point."""
state, reason = ticketpage.bom_state(
{'parts': [], 'strand_linear_ft': None, 'schedules': [],
'unreadable': [('PARTS_LIST', 'InvalidOperationException: boom')]})
assert state == ticketpage.BOM_UNREADABLE
assert 'PARTS_LIST' in reason and 'boom' in reason
assert 'could not be read' in reason
# and it says out loud that this is OUR failure, not the ticket's
assert 'may carry one' in reason
absent, absent_reason = ticketpage.bom_state(
{'parts': [], 'strand_linear_ft': None, 'schedules': [],
'unreadable': []})
assert absent != state
assert absent_reason != reason
def test_nothing_gathered_at_all_is_its_own_state():
state, reason = ticketpage.bom_state(None)
assert state == ticketpage.BOM_UNKNOWN
assert 'no BOM was gathered' in reason
def test_content_plus_an_unreadable_schedule_runs_but_says_so():
state, reason = ticketpage.bom_state(
{'parts': [['2', 'P106', '']], 'strand_linear_ft': None,
'schedules': ['PARTS_LIST'],
'unreadable': [('STRAND_SCHEDULE', 'boom')]})
assert state == ticketpage.BOM_PARTIAL
assert 'STRAND_SCHEDULE' in reason
# partial still RUNS -- a partial read is reported, never withheld
assert ticketpage.bom_not_run_reason(
{'parts': [['2', 'P106', '']], 'schedules': ['PARTS_LIST'],
'unreadable': [('STRAND_SCHEDULE', 'boom')]}, 'x') is None
def test_the_not_run_reason_names_the_comparison_that_wanted_the_bom():
"""A BOM-less ticket is named by EVERYTHING that wanted one."""
bom = {'parts': [], 'strand_linear_ft': None, 'schedules': [],
'unreadable': []}
strand = ticketpage.bom_not_run_reason(bom, "the BOM's strand row")
members = ticketpage.bom_not_run_reason(bom, 'the BOM rows')
assert strand.startswith("the BOM's strand row could not be judged")
assert members.startswith('the BOM rows could not be judged')
assert strand != members
def test_more_than_four_unreadable_schedules_are_counted_not_dumped():
reason = ticketpage.bom_unreadable_sentence(
[('S{0}'.format(i), 'boom') for i in range(7)])
assert reason.startswith('7 schedule(s)')
assert '+3 more' in reason
# --------------------------------- the expensive_geometry run-time switch
def test_the_geometry_switch_has_one_reason_said_one_way():
"""OFF must be attributable to the switch, not to a missing input."""
reason = ticketpage.expensive_geometry_off_reason()
assert reason is ticketpage.EXPENSIVE_GEOMETRY_OFF_REASON
assert 'expensive_geometry' in reason
assert 'OFF' in reason
assert 'run-options.yaml' in reason
# it must not read like a missing input
assert 'could not' not in reason
def test_bed_plan_disposition_keeps_only_plan_bound_bars_in_the_difference():
from vibe import ticketpage as TP
homes = {
'A_FULL': set([TP.BAR_HOME_SECTION_A]),
'B_FULL': set([TP.BAR_HOME_SECTION_B]),
'TRIM': set([TP.BAR_HOME_PLAN]),
'MIXED': set([TP.BAR_HOME_SECTION_A, TP.BAR_HOME_PLAN]),
}
plan, section, unknown = TP.bed_plan_bar_disposition(homes)
assert plan == ['MIXED', 'TRIM']
assert section == ['A_FULL', 'B_FULL']
assert unknown == []
def test_one_unreadable_instance_takes_the_whole_family_out_of_the_flag():
"""Half a family measured is not a licence to flag the family."""
from vibe import ticketpage as TP
homes = {'X': set([TP.BAR_HOME_PLAN, None]), 'Y': set()}
plan, section, unknown = TP.bed_plan_bar_disposition(homes)
assert plan == [] and section == []
assert unknown == ['X', 'Y']
def test_bed_plan_disposition_handles_no_bars_at_all():
from vibe import ticketpage as TP
assert TP.bed_plan_bar_disposition({}) == ([], [], [])
assert TP.bed_plan_bar_disposition(None) == ([], [], [])
# ------------------------------------- 4.5: debond CALLED OUT on the ticket
#
# 4.7 derives where debonding is REQUIRED. 4.5 asks whether the ticket says
# so. These test the second question only: `required_runs` is 4.7's own
# return value, passed straight in, and the geometry is never re-derived.
def _run(x0, x1, strands):
return {'x0': x0, 'x1': x1, 'length_ft': round(x1 - x0, 4),
'strands_in': strands}
def test_45_warns_when_debonding_is_required_and_the_ticket_is_silent():
"""The owner's ruling: required, not called out -> WARN."""
from vibe import ticketpage as TP
sev, code, msg = TP.debond_callout_status(
[_run(0.0, 2.5, [6.0, 12.0])], [], 4.0)
assert sev == 'WARN' and code == 'STRAND-CALLOUT'
# names WHAT and WHERE, never a count
assert '2.5 ft' in msg and '0 to 2.5 ft' in msg
assert '6"' in msg and '12"' in msg
assert 'no debond call-out' in msg
def test_45_cannot_judge_when_47_could_not():
"""4.7 stood down -> 4.5 has no requirement to compare anything to, and
must not warn about a requirement nobody established."""
from vibe import ticketpage as TP
assert TP.debond_callout_status(None, [], 4.0) is None
assert TP.debond_callout_status(None, [{'where': 'BED PLAN',
'text': 'DEBOND STRAND'}],
4.0) is None
def test_45_ok_when_called_out_but_says_what_it_did_not_check():
from vibe import ticketpage as TP
sev, code, msg = TP.debond_callout_status(
[_run(0.0, 2.5, [6.0])],
[{'where': 'BED PLAN', 'text': 'DEBOND STRAND'}], 4.0)
assert sev == 'OK' and code == 'STRAND-CALLOUT'
assert 'BED PLAN' in msg and 'DEBOND STRAND' in msg
# the call-out carries no station: the OK must not claim it was verified
assert 'NOT VERIFIED' in msg
def test_45_nothing_required_is_judged_and_found_nothing_not_silence():
from vibe import ticketpage as TP
sev, code, msg = TP.debond_callout_status([], [], 4.0)
assert sev == 'OK' and code == 'STRAND-CALLOUT'
assert '4 ft' in msg
def test_45_does_not_warn_about_a_callout_nobody_required():
"""Only one direction warns (owner's ruling). A ticket that debonds
where the 4 ft trigger does not fire is NOT flagged -- the plant's own
fixed each-end debond standard is a different rule and is not merged."""
from vibe import ticketpage as TP
sev, code, msg = TP.debond_callout_status(
[], [{'where': 'BED PLAN', 'text': 'DEBOND STRAND'}], 4.0)
assert sev == 'OK'
assert 'not judged' in msg
def test_45_names_at_most_a_readable_number_of_runs():
from vibe import ticketpage as TP
runs = [_run(float(i), i + 0.5, [6.0]) for i in range(9)]
sev, code, msg = TP.debond_callout_status(runs, [], 4.0)
assert sev == 'WARN'
assert '+5 more' in msg
# ------------------------------ 2.4: the extras drawn vs the extras counted
def _extra(family='LEDGE', volume=8.99, mark=None, eid='1234'):
return {'family': family, 'mark': mark, 'id': eid, 'volume_ft3': volume}
OKBOM = ('ok', None)
PARTIALBOM = ('partial', '1 schedule(s) could not be read -- BOM: boom')
ABSENTBOM = ('absent', 'this ticket carries no schedule at all')
UNREADBOM = ('unreadable', '1 schedule(s) on this ticket could not be read')
UNKNOWNBOM = ('unknown', 'no BOM was gathered for this ticket at all')
def test_24_flags_an_extra_the_ticket_names_that_the_model_does_not_carry():
"""THE case 2.2 cannot see: the ledge is off the model, so it is missing
from all three weights, which then agree with each other."""
from vibe import ticketpage as TP
out = TP.cast_extra_agreement(
[], [{'where': 'Elevation Front', 'text': 'SLAB SEAT LEDGE',
'token': 'LEDGE'}], OKBOM, ['LEDGE'])
assert len(out) == 1
sev, code, msg = out[0]
assert sev == 'WARN' and code == 'CAST-EXTRA'
assert 'Elevation Front' in msg and 'SLAB SEAT LEDGE' in msg
assert 'agree with each other' in msg
def test_24_flags_an_extra_in_the_weight_that_the_ticket_never_names():
from vibe import ticketpage as TP
out = TP.cast_extra_agreement([_extra()], [], OKBOM, ['LEDGE'])
assert len(out) == 1
sev, _, msg = out[0]
assert sev == 'WARN'
assert '1 x LEDGE' in msg and '8.99 ft3' in msg
assert 'nothing on this ticket names it' in msg
def test_24_flags_an_extra_whose_volume_could_not_be_read():
"""Drawn and NOT counted: it contributed nothing to 2.2's reconstruction
and 2.2 has no way to say so."""
from vibe import ticketpage as TP
out = TP.cast_extra_agreement(
[_extra(volume=None, mark='L1')],
[{'where': 'BOM row', 'text': '1 LEDGE', 'token': 'LEDGE'}],
OKBOM, ['LEDGE'])
assert len(out) == 1
sev, _, msg = out[0]
assert sev == 'WARN'
assert 'LEDGE (L1)' in msg
assert 'drawn and not counted' in msg
def test_24_agrees_when_both_sides_name_the_same_extra():
from vibe import ticketpage as TP
out = TP.cast_extra_agreement(
[_extra()],
[{'where': 'BOM row', 'text': '1 LEDGE', 'token': 'LEDGE'}],
OKBOM, ['LEDGE'])
assert [o[0] for o in out] == ['OK']
assert '1 x LEDGE' in out[0][2] and 'BOM row' in out[0][2]
def test_24_says_out_loud_what_it_cannot_detect():
"""Nothing on either side is a real OK, but the blind spot the checklist
named -- an extra missing from BOTH -- must not be claimed as checked."""
from vibe import ticketpage as TP
out = TP.cast_extra_agreement([], [], OKBOM, ['LEDGE'])
assert out[0][0] == 'OK'
assert 'cannot be detected here' in out[0][2]
def test_24_stands_down_when_the_bom_could_not_be_read():
"""An unreadable BOM may carry the very row that names the ledge, so
'the ticket does not name it' is not assertable. NOT the same as an
ABSENT BOM, which is a fact about the ticket."""
from vibe import ticketpage as TP
assert TP.cast_extra_agreement([_extra()], [], UNREADBOM,
['LEDGE']) is None
assert TP.cast_extra_agreement([_extra()], [], UNKNOWNBOM,
['LEDGE']) is None
def test_24_still_reports_the_unreadable_volume_when_the_bom_is_unreadable():
"""That finding needs no ticket at all -- but it must say the ticket
comparison did not happen, rather than let the row read as judged."""
from vibe import ticketpage as TP
out = TP.cast_extra_agreement([_extra(volume=None)], [], UNREADBOM,
['LEDGE'])
assert [o[0] for o in out] == ['WARN', 'WARN']
assert 'NOT compared' in out[1][2]
def test_24_judges_an_absent_bom_from_view_text_and_says_so():
from vibe import ticketpage as TP
out = TP.cast_extra_agreement([_extra()], [], ABSENTBOM, ['LEDGE'])
assert out[0][0] == 'WARN'
assert 'no BOM' in out[0][2] and 'text drawn on its views' in out[0][2]
def test_24_carries_the_partial_bom_caveat_into_its_message():
from vibe import ticketpage as TP
out = TP.cast_extra_agreement([_extra()], [], PARTIALBOM, ['LEDGE'])
assert out[0][0] == 'WARN'
assert 'could not be read' in out[0][2]
def test_24_cannot_judge_without_the_plants_cast_family_words():
from vibe import ticketpage as TP
assert TP.cast_extra_agreement([], [], OKBOM, []) is None
# ==================================================================
# THE RESTYLE: order, colour, muting, and the lines behind a count
# ==================================================================
#
# All of this is presentation, and the point of every test here is that
# presentation NEVER changes the data. The five status words
# `checklist_states` produces are what the plain-text and JSON consumers
# read; colour, folding and muting sit on top of them and are not allowed
# to reach down.
def test_checklist_is_in_numerical_order():
"""5.4 was listed before 5.3 until 2026-08-30.
The owner found it by reading a WP133 run. A checklist is read as a
checklist -- a reader who cannot find 5.3 where 5.3 belongs starts
doubting the row is there at all -- so the whole table is held to
numerical order, not just the pair that was wrong.
"""
def key(num):
major, minor = num.split('.')
return (int(major), int(minor))
nums = [item[0] for item in ticketpage.CHECKLIST_ITEMS]
assert nums == sorted(nums, key=key)
assert nums.index('5.3') < nums.index('5.4')
def test_every_checklist_number_is_unique():
nums = [item[0] for item in ticketpage.CHECKLIST_ITEMS]
assert len(nums) == len(set(nums))
# ----------------------------------------------------------- the colours
def test_ok_is_green():
assert ticketpage.state_color('OK') == 'green'
def test_warn_and_for_engineer_are_yellow():
assert ticketpage.state_color('WARN') == 'yellow'
assert ticketpage.state_color('FOR ENGINEER') == 'yellow'
def test_not_run_is_red_exactly_like_fail():
"""The owner's grouping, and the founding principle as colour.
An unrun check tells you nothing about the piece. If NOT RUN were grey
or blue -- calmer than FAIL -- the page would be back to letting silence
read as a pass, which is the one failure this whole tool exists to
prevent. Anyone softening this has to delete this test first, and the
name is here so they know what they are deleting.
"""
assert ticketpage.state_color('FAIL') == 'red'
assert ticketpage.state_color('NOT RUN') == 'red'
assert (ticketpage.state_color('NOT RUN') ==
ticketpage.state_color('FAIL'))
def test_an_unrecognised_status_is_red_not_grey():
assert ticketpage.state_color('WHATEVER') == 'red'
assert ticketpage.state_color('') == 'red'
assert ticketpage.state_color(None) == 'red'
def test_colour_never_touches_the_status_word():
"""Colour is presentation; the word is the data.
The plain-text and JSON consumers read the word. Rendering a row must
leave it spelled exactly as `checklist_states` produced it.
"""
rows = [('1.1', 'a', 'A', 'NOT RUN', 'why'),
('1.2', 'b', 'A', 'OK', None)]
html = ticketpage.checklist_table_html(rows)
text = ticketpage.checklist_table_text(rows)
for blob in (html, text):
assert 'NOT RUN' in blob
assert 'OK' in blob
assert [r[3] for r in rows] == ['NOT RUN', 'OK']
# ------------------------------------------------------------- the muting
def _know(muted):
return {'checks': {'muted': muted}}
def _get(tree, dotted, default=None):
node = tree
for part in dotted.split('.'):
if not isinstance(node, dict) or part not in node:
return default
node = node[part]
return default if node is None else node
def test_muted_checks_reads_the_map():
out = ticketpage.muted_checks(_know({'3.3': 'no piece has one'}),
getter=_get)
assert out == {'3.3': 'no piece has one'}
def test_no_muting_configured_returns_every_row():
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why')]
kept, notes = ticketpage.apply_muting(rows, {})
assert kept == rows
assert notes == []
def test_a_muted_item_stays_on_the_table_in_grey_and_is_listed():
"""Muting is a volume control, not a switch.
This ASSERTED THE OPPOSITE until the mute UI landed: a muted row used
to be dropped from the table. A row that vanishes cannot say it ran, so
a reader could not tell a muted-and-fine item from one nobody built.
"""
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why'),
('1.1', 'name', 'A', 'OK', None)]
kept, notes = ticketpage.apply_muting(rows, {'3.3': 'no piece has one'})
assert [r[0] for r in kept] == ['3.3', '1.1']
assert kept[0][3] == ticketpage.MUTED_STATE
assert ticketpage.state_color(kept[0][3]) == 'grey'
assert notes[0][0] == '3.3'
assert 'no piece has one' in ticketpage.muting_lines(notes)[0]
def test_a_muted_row_still_says_what_it_really_reported():
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'no release rule on this job')]
kept, _notes = ticketpage.apply_muting(rows, {'3.3': 'never applies'})
note = kept[0][4]
assert 'NOT RUN' in note # the real answer survives
assert 'never applies' in note # and why it is quiet
assert 'no release rule on this job' in note # and the original note
def test_a_muted_row_keeps_every_other_field_of_the_row():
rows = [('3.3', 'ratio', 'B', 'OK', 'fine', 'an extra field')]
kept, _notes = ticketpage.apply_muting(rows, {'3.3': 'never applies'})
assert kept[0][0] == '3.3'
assert kept[0][1] == 'ratio'
assert kept[0][2] == 'B'
assert kept[0][5] == 'an extra field'
def test_a_muted_row_is_its_own_bucket_in_the_tally_and_not_a_pass():
"""OPEN QUESTION flagged to the owner: own bucket, or folded into OK?
Built as its own bucket, which is the honest default -- a muted row is
a row nobody read, and counting it as a pass is the quiet lie this
table exists to stop.
"""
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why'),
('1.1', 'name', 'A', 'OK', None)]
kept, _notes = ticketpage.apply_muting(rows, {'3.3': 'no piece has one'})
counts = ticketpage.checklist_tally(kept)
assert counts == {ticketpage.MUTED_STATE: 1, 'OK': 1}
assert counts.get('OK') == 1
headline = ticketpage.checklist_headline(counts)
assert '1 ' + ticketpage.MUTED_STATE in headline
# Quietest last: OK is named before MUTED.
assert headline.index('OK') < headline.index(ticketpage.MUTED_STATE)
def test_the_muted_state_is_grey_and_grey_is_reachable():
"""`STATE_COLORS['grey']` existed and nothing could ever reach it."""
assert ticketpage.GREY_STATES == (ticketpage.MUTED_STATE,)
assert ticketpage.state_color(ticketpage.MUTED_STATE) == 'grey'
assert ticketpage.STATE_COLORS['grey']
def test_an_unknown_status_is_still_red_and_never_grey():
"""The rule grey must not weaken: a state nobody recognises is a state
nobody judged, and the safe reading of that is the loud one."""
assert ticketpage.state_color('SOMETHING NEW') == 'red'
assert ticketpage.state_color('') == 'red'
assert ticketpage.state_color(None) == 'red'
assert ticketpage.state_color('NOT RUN') == 'red'
def test_the_colour_key_names_the_grey_so_it_cannot_read_as_skipped():
html = ticketpage.checklist_key_html()
assert ticketpage.MUTED_STATE in html
assert ticketpage.STATE_COLORS['grey'][0] in html
def test_a_muted_row_renders_grey_in_the_table():
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why')]
kept, _notes = ticketpage.apply_muting(rows, {'3.3': 'no piece has one'})
html = ticketpage.checklist_table_html(kept)
assert ticketpage.STATE_COLORS['grey'][0] in html
assert ticketpage.MUTED_STATE in html
def test_muting_cannot_hide_a_fail():
"""Muting quietens a permanent NOT RUN. It never softens a defect.
Judgement call (Claude 2026-08-30): the brief said a muted check leaves
the table entirely, and this narrows that by exactly one case -- FAIL.
A job file that could grey out a failure is a job file that eventually
will. Now that muting repaints rather than removes, the invariant has
a second half: the FAIL keeps its own WORD as well as its place, so it
is still red and still counted as a failure.
"""
rows = [('3.3', 'ratio', 'B', 'FAIL', 'it is out of band')]
kept, notes = ticketpage.apply_muting(rows, {'3.3': 'never applies'})
assert [r[0] for r in kept] == ['3.3']
assert kept[0][3] == 'FAIL'
assert kept[0] == rows[0] # untouched, not repainted
assert ticketpage.state_color(kept[0][3]) == 'red'
assert 'FAILED' in notes[0][3]
assert ticketpage.muting_problem(notes)
def test_muting_a_number_that_is_not_an_item_is_reported():
rows = [('1.1', 'name', 'A', 'OK', None)]
kept, notes = ticketpage.apply_muting(rows, {'9.9': 'typo'})
assert kept == rows
assert notes[0][0] == '9.9'
assert 'MUTES NOTHING' in notes[0][3]
assert ticketpage.muting_problem(notes)
def test_a_mute_with_no_reason_opens_the_section():
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why')]
_kept, notes = ticketpage.apply_muting(rows, {'3.3': ''})
assert ticketpage.muting_problem(notes)
def test_tidy_muting_stays_folded():
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why')]
_kept, notes = ticketpage.apply_muting(rows, {'3.3': 'no piece has one'})
assert ticketpage.muting_problem(notes) is None
def test_muting_takes_rows_and_returns_rows_only():
"""It cannot reach the findings, because it is never handed them.
The JSON writer and the clean/failing/warning tally both read
`r['findings']`, which never passes through here. This test pins the
signature that makes that true.
"""
findings = [_F('FAIL', 'FCI-RATIO', 'out of band')]
rows = [('3.3', 'ratio', 'B', 'NOT RUN', 'why')]
before = list(findings)
ticketpage.apply_muting(rows, {'3.3': 'never applies'})
assert findings == before
assert len(findings) == 1
# ------------------------------------------- the lines behind a count (8.1)
def test_81_detail_enumerates_every_line():
""""都哪些lines not interpreted?" -- the count resolves to the things."""
f = _F('WARN', 'SPEC-UNRESOLVED',
'4 line(s) of the design summary were not interpreted: '
'STRAND PATTERN | 1/2" DIA. STR. | #3S @ 18" O.C. | (2)@3" EA. END')
out = ticketpage.checklist_details([f])
assert out['8.1'] == ['STRAND PATTERN', '1/2" DIA. STR.',
'#3S @ 18" O.C.', '(2)@3" EA. END']
def test_a_single_line_needs_no_detail_block():
f = _F('WARN', 'SPEC-UNRESOLVED',
'1 line(s) of the design summary were not interpreted: ONE THING')
assert ticketpage.checklist_details([f]) == {}
def test_details_ignore_findings_that_do_not_enumerate():
assert ticketpage.checklist_details([_F('WARN', 'CLASH', 'a: b | c')]) == {}
# ------------------------------------------------------ the rendered table
def test_the_table_carries_a_colour_per_state():
rows = [('1.1', 'a', 'A', 'OK', None),
('1.2', 'b', 'A', 'WARN', 'look'),
('1.3', 'c', 'A', 'NOT RUN', 'nobody looked')]
html = ticketpage.checklist_table_html(rows)
assert ticketpage.STATE_COLORS['green'][0] in html
assert ticketpage.STATE_COLORS['yellow'][0] in html
assert ticketpage.STATE_COLORS['red'][0] in html
def test_the_table_uses_nothing_the_ie_panel_cannot_render():
"""pyRevit's output is an IE-engine WebBrowser.
`<details>`/`<summary>` do not exist there and fail SILENTLY -- the
"collapsed" content just stays visible. No HTML5 element, no flexbox,
no `rgba()`, anywhere in the table or its style.
"""
html = ticketpage.checklist_table_html(
[('1.1', 'a', 'A', 'OK', None)], table_id='X')
for banned in ('<details', '<summary', 'flex', 'rgba('):
assert banned not in html
for banned in ('display:flex', 'display: flex', 'rgba('):
assert banned not in ticketpage.CHECKLIST_STYLE
def test_a_row_with_a_short_note_carries_no_script_at_all():
"""THE CONTRACT THAT USED TO COVER THE WHOLE TABLE, NARROWED TWICE NOW.
Original: the table uses no script at all, so if every toggle on the
page is dead the checklist is still there in full. The note fold
(2026-08-30) narrowed that -- a folded note's text is all in the DOM,
selectable and copyable, but a dead toggle leaves it visually clipped --
and the narrowed promise used to read: a row whose status is RED, and a
row whose note is short, are plain HTML with no handler on them.
The owner overruled the RED half of that on 2026-08-31 ("notes太长的话
默认折叠" -- see `test_every_row_folds_a_long_note_now`): a long red note
folds like any other long note today, so it DOES carry `onclick`. What
is still true, on every status including red, is the other half -- a
note that already fits its item cell gets no control at all, because a
fold that would not hide anything is not drawn.
"""
plain = [('1.1', 'a', 'A', 'OK', None),
('1.2', 'b', 'A', 'WARN', 'short enough to read at a glance'),
('1.3', 'c', 'A', 'FAIL', 'also short enough for one glance'),
('1.4', 'd', 'A', 'NOT RUN', 'still short enough to read here')]
html = ticketpage.checklist_table_html(plain, table_id='X')
assert 'onclick' not in html
assert 'wt-ck-note-clip' not in html
# ------------------------------------------------- the note fold (08-30)
#
# Owner: "不要有截断,item不要折叠,需要一行就一行,需要两行就两行,但是note做成
# 默认折叠显示跟item一样的行数。如果note需要40行,但是item只有两行,那么默认显示
# 前两行,然后可以打开折叠。"
_LONG = 'the whole of it, ' * 40
def test_a_long_yellow_note_folds_and_keeps_every_character():
html = ticketpage.checklist_table_html(
[('4.5', 'Debonded strands called out where the design debonds',
'C', 'WARN', _LONG)], table_id='X')
assert 'wt-ck-note-clip' in html
assert _LONG in html # folded is not shortened
assert '...' not in html
def test_the_item_cell_is_never_folded():
""""item不要折叠" -- the question column takes the height it needs."""
long_item = 'a question long enough to wrap several times over ' * 4
html = ticketpage.checklist_table_html(
[('4.5', long_item, 'C', 'WARN', _LONG)], table_id='X')
item_cell = html.split('<td class="wt-ck-grade">')[0]
assert long_item in item_cell
assert 'max-height' not in item_cell
assert 'wt-ck-note-clip' not in item_cell
def test_the_collapsed_note_is_as_tall_as_its_own_rows_item():
"""Not a fixed number of lines: the row's own item decides. A one-line
item collapses its note to one line, a two-line item to two."""
one_line = 'Short item'
two_lines = 'x' * (ticketpage.ITEM_CHARS_PER_LINE + 5)
assert ticketpage.item_line_estimate(one_line) == 1
assert ticketpage.item_line_estimate(two_lines) == 2
short = ticketpage.checklist_table_html(
[('1.1', one_line, 'A', 'WARN', _LONG)], table_id='X')
tall = ticketpage.checklist_table_html(
[('1.1', two_lines, 'A', 'WARN', _LONG)], table_id='Y')
assert 'max-height:{0:.2f}em'.format(ticketpage.NOTE_LINE_HEIGHT) in short
assert 'max-height:{0:.2f}em'.format(2 * ticketpage.NOTE_LINE_HEIGHT) \
in tall
def test_the_fold_height_and_the_style_agree_on_what_a_line_is():
"""The height is computed as lines x NOTE_LINE_HEIGHT in `em`, so the
CSS `line-height` has to be the same number or "two lines" silently
stops meaning two lines."""
assert 'line-height: {0};'.format(ticketpage.NOTE_LINE_HEIGHT) \
in ticketpage.CHECKLIST_STYLE
def test_every_row_folds_a_long_note_now():
"""OVERRULED, owner, 2026-08-31: "notes太长的话默认折叠" -- long notes
fold by default, and the instruction names no status word. It replaces
the red exemption this test used to pin (`test_a_red_row_never_folds_
no_matter_how_long_its_note`, standing 2026-08-30 to 2026-08-31): row
probes, shared-silence cross-references and dated mute reasons have
since made checklist notes long as a matter of course, mostly on NOT
RUN -- the very status that used to be exempt -- and the owner is now
looking at walls of text on his screen, not a rare long WARN.
The new contract, pinned here:
* EVERY status folds a note that exceeds its item's height, FAIL and
NOT RUN included. No status is special-cased out any more.
* NOTHING IS EVER TRUNCATED. `_LONG` survives whole in the HTML on
every status -- the tombstone this suite already keeps
(`NOTE_CAP` / `capped_note` are gone for good) is untouched by this
ruling and stays a separate guarantee.
* THE FOLD CONTROL IS PRESENT AND STATES WHAT IS BEHIND IT. `[+] the
whole note (N characters)` -- a reader is never left guessing
whether a red row is hiding anything, only how much.
* Folding is not hiding: the state colour and the first `item_lines`
lines stay visible on every row before any click.
"""
for status in ticketpage.RED_STATES + ticketpage.YELLOW_STATES:
html = ticketpage.checklist_table_html(
[('1.1', 'Short item', 'A', status, _LONG)], table_id='X')
assert 'wt-ck-note-clip' in html, status
assert 'onclick' in html, status
assert _LONG in html, status # folded is not truncated
assert '...' not in html, status
assert '[+] the whole note ({0} characters)'.format(len(_LONG)) \
in html, status
assert ticketpage.note_folds(_LONG, 1, 'WARN') is True
assert ticketpage.note_folds(_LONG, 1, 'FAIL') is True
assert ticketpage.note_folds(_LONG, 1, 'NOT RUN') is True
def test_a_note_shorter_than_its_item_gets_no_control_that_does_nothing():
html = ticketpage.checklist_table_html(
[('1.1', 'A fairly long question that wraps onto two lines here',
'A', 'WARN', 'brief')], table_id='X')
assert 'wt-ck-note-clip' not in html
assert 'wt-ck-note-more' not in html
def test_the_file_link_is_rendered_outside_the_fold():
"""7.1's evidence IS the link. A fold that swallowed it would hide the
one thing the note exists to hand over."""
note = _LONG + ticketpage.NOTE_FILE_SEP + _CSV
html = ticketpage.checklist_table_html(
[('7.1', 'Interference check', 'B', 'WARN', note)], table_id='X')
clip = html.split('class="wt-ck-note-body wt-ck-note-clip"')[1]
clip = clip.split('</div>')[0]
assert _CSV not in clip # not inside the clipped div
assert 'wt-ck-note-path' in html
assert '>' + _CSV + '</a>' in html
def test_the_toggle_is_the_fold_convention_this_tool_already_uses():
"""One mechanism, extended -- not a second one. Inline `onclick`, an
element id, a header at `id + '_h'`, and the same [+]/[-] marker the
report folds swap."""
from vibe import foldreport
html = ticketpage.checklist_table_html(
[('4.5', 'Short item', 'C', 'WARN', _LONG)], table_id='X')
ident = ticketpage.note_fold_id('X', '4.5')
assert 'id="{0}"'.format(ident) in html
assert 'id="{0}_h"'.format(ident) in html
assert 'onclick="wtClip(\'{0}\')"'.format(ident) in html
assert '[+]' in html
assert 'function wtClip' in foldreport.SCRIPT
assert 'function wtFold' in foldreport.SCRIPT # still there
for banned in ('addEventListener', 'classList', 'querySelector'):
assert banned not in foldreport.SCRIPT
def test_the_control_says_how_much_is_behind_it():
"""A reader is never left guessing whether anything is folded away --
which is the one thing the old '...' did do."""
html = ticketpage.checklist_table_html(
[('4.5', 'Short item', 'C', 'WARN', _LONG)], table_id='X')
assert '({0} characters)'.format(len(_LONG)) in html
def test_two_tickets_on_one_page_do_not_share_a_fold_id():
"""A run prints one table per ticket and every one has a row 4.5. One
id twice means one toggle opens whichever the browser found first."""
row = [('4.5', 'Short item', 'C', 'WARN', _LONG)]
first = ticketpage.checklist_table_html(row)
second = ticketpage.checklist_table_html(row)
ids = []
for html in (first, second):
ids.append(html.split('<div id="')[1].split('"')[0])
assert ids[0] != ids[1]
assert ticketpage.note_fold_id('X', '4.5') \
!= ticketpage.note_fold_id('Y', '4.5')
def test_the_text_file_has_no_fold_and_no_markers():
""""The unfolded file copy keeps everything" -- it is what the owner
falls back to when the panel misbehaves."""
out = ticketpage.checklist_table_text(
[('4.5', 'Short item', 'C', 'WARN', _LONG)], width=70)
for markup in ('[+]', 'wt-ck', 'onclick', 'max-height', '...'):
assert markup not in out
# counted a word at a time: `textwrap` breaks at spaces, so a phrase
# spans lines while a word never does
assert out.count('whole') == 40
def test_the_table_is_bigger_than_the_panel_default():
""""字儿可以再大一点" -- the panel's own body text is 8pt."""
assert '11pt' in ticketpage.CHECKLIST_STYLE
assert '12pt' in ticketpage.CHECKLIST_STYLE
def test_a_note_cell_is_allowed_to_wrap_rather_than_be_cut():
""""一行放不下的就多来几行"."""
assert 'white-space: normal' in ticketpage.CHECKLIST_STYLE
assert 'word-wrap: break-word' in ticketpage.CHECKLIST_STYLE
def test_the_table_escapes_a_mark_containing_markup():
html = ticketpage.checklist_table_html(
[('1.1', 'a<b>c', 'A', 'OK', 'x & y')])
assert '&lt;b&gt;' in html
assert 'x &amp; y' in html
def test_detail_lines_are_rendered_under_their_row():
rows = [('8.1', 'unresolved', 'C', 'WARN', '4 line(s) ...')]
html = ticketpage.checklist_table_html(
rows, {'8.1': ['STRAND PATTERN', '#3S @ 18" O.C.']})
assert 'STRAND PATTERN' in html
assert '#3S @ 18&quot; O.C.' in html or '#3S @ 18" O.C.' in html
# --------------------------------- a detail block is a CHILD, not a band
#
# Owner, reading the WP296 run: "8.1 跟 8.2 之间那是啥?我总感觉它在的表格的
# 位置不太对。" It was 8.1's own detail block, and every visual cue said
# otherwise: full width from the table's left edge, painted in the parent's
# status colour, and a rule between it and the row it belonged to.
def test_the_detail_block_does_not_start_at_the_tables_left_edge():
"""It begins under the ITEM column; the # column is left blank.
A block that starts at x=0 is a row. A block that starts where the
parent's text starts is that parent's second half.
"""
html = ticketpage.checklist_table_html(
[('8.1', 'unresolved', 'C', 'WARN', 'count: a | b')],
{'8.1': ['a', 'b']})
assert 'wt-ck-detail-pad' in html
assert 'colspan="4"' in html
assert 'colspan="5"' not in html
def test_nothing_is_drawn_between_a_row_and_its_own_detail():
"""The parent loses its bottom rule, so the two read as one block."""
html = ticketpage.checklist_table_html(
[('8.1', 'unresolved', 'C', 'WARN', 'count: a | b')],
{'8.1': ['a', 'b']})
assert '<tr class="wt-ck-open">' in html
assert '.wt-ck tr.wt-ck-open td { border-bottom: none; }' \
in ticketpage.CHECKLIST_STYLE
def test_no_column_rule_is_written_so_weakly_that_it_never_binds():
"""THE BUG THAT MADE THE BLOCK LOOK LIKE A ROW, GUARDED.
`.wt-ck td` is (class + element); a bare `.wt-ck-detail` is (class), so
the bare form loses every property the td rule also sets -- and loses it
silently, declaration present, browser discarding it. The detail block
asked for a 51px left padding for months and rendered at the td rule's
7px: full width, at the table's left edge, indented by nothing.
Any rule for a cell class must therefore be qualified with `td`.
"""
cell_classes = ('wt-ck-num', 'wt-ck-grade', 'wt-ck-state',
'wt-ck-detail', 'wt-ck-detail-pad')
checked = 0
for line in ticketpage.CHECKLIST_STYLE.split('\n'):
if '{' not in line:
continue
selector = line.split('{')[0].strip()
if not selector.startswith('.wt-ck'):
continue
target = selector.split()[-1] # what the rule actually paints
for cls in cell_classes:
if target.endswith('.' + cls):
assert target == 'td.' + cls, selector
checked += 1
assert checked == len(cell_classes) # and every one of them is there
def test_a_row_with_no_detail_is_not_opened():
html = ticketpage.checklist_table_html([('8.2', 'b', 'A', 'OK', None)])
assert 'wt-ck-open' not in html
assert 'wt-ck-detail' not in html
def test_the_detail_block_is_not_painted_in_the_parents_status_colour():
"""A pale yellow band the full width of the table IS the thing he saw.
The tie to the parent is its status INK on the left rule, not its ground
across the whole width.
"""
ink, ground = ticketpage.STATE_COLORS['yellow']
html = ticketpage.checklist_table_html(
[('8.1', 'unresolved', 'C', 'WARN', 'count: a | b')],
{'8.1': ['a', 'b']})
detail = html.split('wt-ck-detail-pad')[1]
assert 'background:{0}'.format(ground) not in detail
assert 'border-left-color:{0}'.format(ink) in detail
def test_the_detail_block_is_smaller_than_the_row_above_it():
"""10pt under the table's own 11pt -- and it has to actually bind."""
assert '.wt-ck td.wt-ck-detail { font-size: 10pt;' \
in ticketpage.CHECKLIST_STYLE
assert '.wt-ck td { padding: 5px 7px' in ticketpage.CHECKLIST_STYLE
assert 'font-size: 11pt;' in ticketpage.CHECKLIST_STYLE
def test_the_status_column_is_the_biggest_thing_in_the_row():
""""OK用绿色..." -- and 12pt, which until now the browser discarded."""
assert '.wt-ck td.wt-ck-state { font-size: 12pt;' \
in ticketpage.CHECKLIST_STYLE
def test_the_note_does_not_repeat_the_lines_printed_under_it():
"""Said once. The note stops at the colon; the bullets carry the rest."""
note = ('4 line(s) of the design summary were not interpreted: '
'STRAND PATTERN | 1/2" DIA. STR.')
html = ticketpage.checklist_table_html(
[('8.1', 'unresolved', 'C', 'WARN', note)],
{'8.1': ['STRAND PATTERN', '1/2" DIA. STR.']})
assert html.count('STRAND PATTERN') == 1
assert 'were not interpreted:' in html
def test_the_note_is_left_whole_when_nothing_is_printed_under_it():
note = '1 line(s) of the design summary were not interpreted: ONE THING'
assert ticketpage.note_lead(note, None) == note
assert ticketpage.note_lead(note, []) == note
def test_the_text_report_says_it_once_too():
out = ticketpage.checklist_table_text(
[('8.1', 'unresolved', 'C', 'WARN', 'count: A THING | B THING')],
{'8.1': ['A THING', 'B THING']})
assert out.count('A THING') == 1
assert '- A THING' in out
# ------------------------------- a note that hands the reader a file (7.1)
#
# "note 那边应该是一个文件的链接,单击打开那个文件". The visible text stays the
# real Windows path -- that is what gets copied into Explorer -- and only the
# href takes the `file:///` form, exactly as specsave/PanelLiftCheck do it.
_CSV = r'C:\jobs\Some Job\_ticketqc\interference-PNL1.csv'
def _file_note(count=4, members=142, path=_CSV):
return ('{0} interference(s) among the {1} member(s) of this piece\'s '
'own assembly{2}{3}'.format(count, members,
ticketpage.NOTE_FILE_SEP, path))
def test_a_note_ending_in_a_path_renders_as_a_link():
html = ticketpage.checklist_table_html(
[('7.1', 'Interference check', 'B', 'WARN', _file_note())])
assert 'href="file:///C:/jobs/Some Job/_ticketqc/interference-PNL1.csv"' \
in html
# the visible text is the real path, backslashes and all
assert '>' + _CSV + '</a>' in html
def test_a_note_with_no_path_is_not_linkified():
html = ticketpage.checklist_table_html(
[('7.1', 'Interference check', 'B', 'WARN', 'nothing to open')])
assert '<a href' not in html
# ------------------------------------------ nothing truncates a note now
#
# Owner, 2026-08-30, having read a live 4.5: "4.5被截断了。我经常在notes里面有
# 截断。不要有截断". These three tests replace the three that pinned the cap's
# behaviour (`test_the_cap_never_cuts_the_path_in_half`,
# `..._still_bites_on_the_prose_in_front_of_the_path`,
# `test_an_ordinary_long_note_is_still_capped`). The cap is gone, and what
# is pinned now is its absence.
def test_a_long_note_reaches_the_row_whole():
"""`checklist_states` used to run every message through a 180-character
cap, so the ROW carried a stump and the HTML table, the plain-text file
and the JSON all inherited it. 4.5's real message is over 300."""
from vibe import ticketpage as TP
message = ('debonding is required here -- ' + 'w' * 300 +
' -- and this half is the half that vanished')
rows = TP.checklist_states(
[_F('WARN', 'STRAND-CALLOUT', message)],
log=[('check_debond_callout', 'RAN', None)])
note = _row(rows, '4.5')[4]
assert note == message
assert '...' not in note
def test_no_cap_survives_anywhere_in_the_module():
"""A tombstone. If `capped_note` comes back, so does the defect."""
assert not hasattr(ticketpage, 'capped_note')
assert not hasattr(ticketpage, 'NOTE_CAP')
def test_the_whole_note_reaches_the_html_and_the_text_alike():
"""Both consumers, one source. The panel may FOLD it; the file may WRAP
it; neither may lose a character of it."""
message = 'alpha ' + 'z' * 400 + ' omega'
rows = [('4.5', 'Debonded strands called out where the design debonds',
'C', 'WARN', message)]
html = ticketpage.checklist_table_html(rows, table_id='X')
text = ticketpage.checklist_table_text(rows, width=70)
for blob in (html, text):
assert 'alpha' in blob
assert 'omega' in blob
assert '...' not in blob
assert 'z' * 400 in html # in the DOM, uncut
assert text.count('z') == 400 # wrapped, not trimmed
# ------------- no row may print NOT RUN with an empty reason (2026-08-31)
#
# Seven checklist rows name no check function: they are answered by finding
# codes `ticketqc.check_ticket` emits. `run_all`'s skip probes are keyed by
# CHECK, so none of that machinery could reach them, and on a live run six of
# them (2.1, 2.3, 3.1, 3.2, 4.1, 4.2) came back NOT RUN with `reason: null`
# and printed as a bare red row with nothing under it. A row that cannot say
# why it is quiet gives the reader nothing to disagree with -- which is how a
# page of real refusals gets read as noise.
def _every_row(findings=(), log=()):
return dict((r[0], r) for r in ticketpage.checklist_states(
list(findings), list(log)))
def test_no_row_can_print_not_run_with_an_empty_reason():
"""THE MECHANICAL GUARANTEE, over the whole table with nothing supplied.
A new code-answered row added with neither a probe nor a declared reason
fails here rather than on the owner's screen."""
for num, row in _every_row().items():
if row[3] != 'NOT RUN':
continue
assert row[4], 'row {0} is NOT RUN with no reason'.format(num)
assert str(row[4]).strip(), num
def test_a_row_answered_only_by_codes_says_so_when_nothing_explained_it():
"""The last rung: no probe spoke and the item declares no reason. It is
reported as the defect it is -- the same ruling as RAN-EMPTY's "no probe
declared" -- never as a blank."""
row = _every_row()['3.1']
assert row[3] == 'NOT RUN'
assert 'FC' in row[4] # the code it waited for
assert 'nothing in this run recorded why' in row[4]
assert 'not a verdict on the piece' in row[4]
def test_a_row_probes_reason_reaches_the_row_through_the_log():
"""`run_all` writes it under `row:<number>` because the log is the one
channel that already reaches every consumer -- no caller had to change
to carry it."""
reason = 'no spec has been extracted for this design -- it carries no fc'
row = _every_row(log=[(ticketpage.ROW_NOTE_PREFIX + '3.1', 'NO-CODE',
reason)])['3.1']
assert row[3] == 'NOT RUN'
assert row[4] == reason
def test_a_code_that_did_arrive_still_beats_any_row_probe():
"""The probe explains a SILENCE. A row that was answered is answered, and
a probe that spoke anyway may not overwrite the verdict."""
finding = _F('OK', 'FC', "f'c 6000 psi")
row = _every_row([finding], [(ticketpage.ROW_NOTE_PREFIX + '3.1',
'NO-CODE', 'would have been quiet')])['3.1']
assert row[3] == 'OK'
assert row[4] is None
def test_four_rows_quiet_for_one_fact_say_so_to_each_other():
"""Rows 4.4 and 4.6 are answered by one check and stand down on one
sentence; four red rows carrying one fact read as four problems. Each
keeps its OWN sentence and gains a cross-reference -- a group summary
that replaced the sentences would trade one silence for another."""
reason = ('no strand geometry on this piece -- strand positions and row '
'spacing could not be judged')
rows = _every_row(log=[('check_strand_layout', 'SKIPPED', reason)])
for num, other in (('4.4', '4.6'), ('4.6', '4.4')):
assert rows[num][4].startswith(reason)
assert 'one fact, not 2 separate problems' in rows[num][4]
assert other in rows[num][4]
def test_a_row_quiet_on_its_own_is_not_told_it_has_company():
reason = 'no dunnage on this piece -- nothing to place under an opening'
rows = _every_row(log=[('check_dunnage_openings', 'SKIPPED', reason)])
assert rows['6.4'][4] == reason
# ---------------- the text report may not drop a character of a note
#
# `last-ticketqc.txt` is documented as the complete record. A run on
# 2026-08-31 wrote it with every CJK character of the job file's mute
# sentences REMOVED -- the ASCII fragments and the gaps between them
# survived, and the same sentences rendered correctly in the panel and in
# `qc-run.json`. A file promised as complete that silently drops half a
# sentence is worse than one that fails to write.
#
# The mechanism is engine-specific and CANNOT be reproduced on the host
# interpreter this suite runs on: `textwrap` munges whitespace with
# `text.translate(<256-entry table>)` for anything it takes for a `str`, and
# IronPython 2.7's `str` holds .NET UTF-16 text, so a character above U+00FF
# has no entry in that table. Hence two tests: one that a non-ASCII note
# survives (true on any engine, and the promise itself), and one that pins
# the SHAPE that keeps it true on the engine that ships.
_CJK_REASON = u'开口未建模 -- muted by the owner'
def test_a_non_ascii_note_reaches_the_text_report_intact():
out = ticketpage.checklist_table_text(
[('3.3', 'release ratio', 'B', ticketpage.MUTED_STATE, _CJK_REASON)])
assert _CJK_REASON in out
def test_a_non_ascii_mute_reason_survives_every_step_to_the_report():
"""Read from the job file, repainted onto the row, wrapped into the
file. Every step, because the loss only has to happen once."""
muted = ticketpage.muted_checks({'checks': {'muted': {'3.3': _CJK_REASON}}},
getter=lambda k, path, default=None:
k.get('checks', {}).get('muted', default))
assert muted['3.3'] == _CJK_REASON
rows, notes = ticketpage.apply_muting(
[('3.3', 'release ratio', 'B', 'NOT RUN', 'no spec')], muted)
assert _CJK_REASON in rows[0][4]
assert notes[0][2] == _CJK_REASON
text = ticketpage.checklist_table_text(rows)
assert _CJK_REASON in text
# and it is TEXT, so the button's one `encode('utf-8')` is all it takes
assert text.encode('utf-8').decode('utf-8') == text
def test_a_non_ascii_note_survives_wrapping_onto_several_lines():
"""The wrap is where the characters were lost, so the long case is the
one that matters -- a short note might never reach `textwrap` at all."""
note = (_CJK_REASON + ' ') * 12
out = ticketpage.checklist_table_text(
[('3.3', 'release ratio', 'B', ticketpage.MUTED_STATE, note)],
width=60)
assert out.count(u'开口未建模') == 12
assert len(out.split('\n')) > 3 # it really did wrap
def test_the_text_report_never_hands_a_note_to_textwraps_translate():
"""A TOMBSTONE, and the only kind of test that can hold here: the defect
lives in an engine this suite does not run on, so what is pinned is the
shape that avoids it -- whitespace flattened before the wrap, `textwrap`
told not to do it again, and no bare `str()` on a note."""
import inspect
def code_of(fn):
"""The function without its docstring -- these docstrings TALK about
`str()`, and a prose mention is not a call."""
return inspect.getsource(fn).replace(fn.__doc__ or '\n\n', '')
src = code_of(ticketpage._wrap_text)
assert 'replace_whitespace=False' in src
assert 'expand_tabs=False' in src
assert "' '.join(_text(value).split())" in src
table = code_of(ticketpage.checklist_table_text)
assert 'str(' not in table
assert '_tw.wrap' not in table # it goes through `_wrap_text`
for name in ('note_lead', 'note_parts', '_muted_row', 'muted_checks'):
assert 'str(' not in code_of(getattr(ticketpage, name)), name
def test_the_text_report_keeps_the_path_on_one_unwrapped_line():
"""`textwrap` would break it at the space in a folder name."""
out = ticketpage.checklist_table_text(
[('7.1', 'Interference check', 'B', 'WARN', _file_note())], width=60)
assert any(line.strip() == _CSV for line in out.split('\n'))
def test_a_hash_in_the_path_does_not_truncate_the_link():
"""`#` starts a URL fragment. One in a folder name kills the href.
The visible text keeps the real `#` -- it is what goes into Explorer.
"""
path = r'C:\jobs\mark#7\pairs.csv'
html = ticketpage.checklist_table_html(
[('7.1', 'x', 'B', 'WARN', _file_note(path=path))])
assert 'href="file:///C:/jobs/mark%237/pairs.csv"' in html
assert '>' + path + '</a>' in html
assert ticketpage.note_href(r'C:\a%b\c.csv') == 'file:///C:/a%25b/c.csv'
def test_note_parts_returns_three_values_either_way():
assert ticketpage.note_parts('plain') == ('plain', '', '')
assert ticketpage.note_parts(None) == ('', '', '')
head, sep, path = ticketpage.note_parts(_file_note())
assert sep == ticketpage.NOTE_FILE_SEP and path == _CSV
def test_the_text_table_wraps_a_long_note_onto_more_lines():
note = 'word ' * 60
out = ticketpage.checklist_table_text(
[('1.4', 'view names', 'B', 'WARN', note)], width=60)
lines = out.split('\n')
assert len(lines) > 3
# nothing was cut: every word survives
assert out.count('word') == 60
def test_the_text_table_prints_the_state_word_in_full():
out = ticketpage.checklist_table_text(
[('3.3', 'ratio', 'B', 'NOT RUN', None)])
assert 'NOT RUN' in out
def test_the_headline_names_the_loud_states_first():
counts = {'OK': 20, 'NOT RUN': 3, 'WARN': 2, 'FAIL': 1}
line = ticketpage.checklist_headline(counts)
assert line.index('1 FAIL') < line.index('3 NOT RUN')
assert line.index('3 NOT RUN') < line.index('2 WARN')
assert line.index('2 WARN') < line.index('20 OK')
def test_the_tally_counts_the_rows_the_reader_sees():
rows = [('1.1', 'a', 'A', 'OK', None), ('1.2', 'b', 'A', 'OK', None),
('1.3', 'c', 'A', 'NOT RUN', 'why')]
assert ticketpage.checklist_tally(rows) == {'OK': 2, 'NOT RUN': 1}
def test_the_key_line_says_what_not_run_means():
"""A red row read as "the tool is broken" gets ignored."""
key = ticketpage.checklist_key_html()
assert 'nobody checked' in key
assert 'NOT RUN' in key
# ==================================================== 5.7 bar length vs stock
#
# Owner, 2026-08-30: "rebars come in 40ft stock. unless overwritten by other
# knowledge, all rebars pieces must be less than 40ft." The number is
# published data (`catalogue.rebar.stock_length_ft`), the length is READ from
# the member.length role and never measured, and the finding is grouped by
# MARK because the live model carries 2,122 elements at or over 40 ft and a
# per-instance report would bury every other check on the ticket.
def _bar(mark, length, family='BAR_FAMILY_A'):
return {'mark': mark, 'family': family, 'length_ft': length}
def test_57_a_bar_at_or_over_stock_warns_and_names_the_mark():
findings, _u, _z, _e = ticketpage.bar_stock_length_status(
[_bar('SR1101', 60.0), _bar('SR310', 11.66)], 40.0)
assert len(findings) == 1
sev, msg = findings[0]
assert sev == 'WARN'
assert 'SR1101' in msg and '60 ft' in msg
assert '40' in msg
assert 'SR310' not in msg # the bar that is fine is not named
def test_57_exactly_at_stock_is_over():
"""40.0 ft cannot be cut from a 40 ft stick either -- >=, not >."""
findings, _u, _z, _e = ticketpage.bar_stock_length_status(
[_bar('SR900', 40.0)], 40.0)
assert findings[0][0] == 'WARN'
def test_57_groups_by_mark_and_counts_the_pieces():
"""2,122 instance findings is noise; "SR1101, 3 pieces" is a ruling."""
findings, _u, _z, _e = ticketpage.bar_stock_length_status(
[_bar('SR1101', 60.0), _bar('SR1101', 60.0), _bar('SR1101', 55.0)],
40.0)
assert len(findings) == 1
assert '1 bar mark(s)' in findings[0][1]
assert '3 piece(s)' in findings[0][1]
def test_57_everything_short_is_a_pass_that_says_how_many():
findings, _u, _z, _e = ticketpage.bar_stock_length_status(
[_bar('SR310', 11.66), _bar('SR324', 4.42)], 40.0)
assert findings[0][0] == 'OK'
assert '2 bar(s)' in findings[0][1]
def test_57_a_missing_length_is_could_not_judge_and_is_named():
"""Never silently skipped: a bar nobody measured is not a bar that passed."""
findings, unread, zero, _e = ticketpage.bar_stock_length_status(
[_bar('SR310', 11.66), _bar('SR777', None)], 40.0)
assert findings[0][0] == 'OK' # decided on the one that was read
assert unread == ['SR777']
assert zero == []
def test_57_zero_is_its_own_state_neither_a_length_nor_an_absence():
"""260 elements on the live model hold the parameter, holding 0.0."""
findings, unread, zero, _e = ticketpage.bar_stock_length_status(
[_bar('SR310', 11.66), _bar('SR9', 0.0), _bar('SR9', 0.0)], 40.0)
assert zero == ['SR9 (2)']
assert unread == []
assert 'SR9' not in findings[0][1] # not passed, and not flagged
def test_57_an_exempt_mark_is_set_aside_and_listed_not_silently_dropped():
findings, _u, _z, exempted = ticketpage.bar_stock_length_status(
[_bar('SR1101', 60.0), _bar('SR310', 11.66)], 40.0,
exempt_marks=['sr1101'])
assert findings[0][0] == 'OK' # nothing left over stock
assert exempted == ['SR1101']
def test_57_exemption_is_exact_never_a_substring():
"""`SR1` must not swallow `SR1101` -- the trap called_out_on_detail hit."""
findings, _u, _z, exempted = ticketpage.bar_stock_length_status(
[_bar('SR1101', 60.0)], 40.0, exempt_marks=['SR1'])
assert exempted == []
assert findings[0][0] == 'WARN'
def test_57_no_stock_length_decides_nothing_at_all():
"""A null key is NOT RUN through the probe, never an assumed 40."""
assert ticketpage.bar_stock_length_status(
[_bar('SR1101', 60.0)], None) == ([], [], [], [])
def test_57_an_unmarked_bar_is_named_by_its_family_not_dropped():
findings, _u, _z, _e = ticketpage.bar_stock_length_status(
[{'mark': None, 'family': 'BAR_FAMILY_B', 'length_ft': 60.0}], 40.0)
assert 'BAR_FAMILY_B' in findings[0][1]
# ============================================ 1.5 call-outs must not overlap
def _note(view, text, box, rotated=False, kind='text note'):
return {'view': view, 'text': text, 'kind': kind, 'box': box,
'rotated': rotated}
def test_15_positive_area_intersection_warns_with_the_size_and_both_texts():
findings, _u, _r = ticketpage.callout_overlap_status(
[_note('Elevation Front', 'SR310 (E.L.)', (0, 0, 1, 1)),
_note('Elevation Front', 'SR324 (F.F.)', (0.5, 0.5, 1.5, 1.5))])
sev, msg = findings[0]
assert sev == 'WARN'
assert 'SR310 (E.L.)' in msg and 'SR324 (F.F.)' in msg
assert '0.5" x 0.5"' in msg
assert 'Elevation Front' in msg
def test_15_touching_edges_are_not_an_overlap():
"""The owner's spec is POSITIVE area. Two boxes sharing a boundary print
clear of each other."""
assert ticketpage.rect_overlap((0, 0, 1, 1), (1, 1, 2, 2)) is None
assert ticketpage.rect_overlap((0, 0, 1, 1), (1, 0, 2, 1)) is None
findings, _u, _r = ticketpage.callout_overlap_status(
[_note('V', 'a', (0, 0, 1, 1)), _note('V', 'b', (1, 0, 2, 1))])
assert findings[0][0] == 'OK'
def test_15_two_views_are_never_compared_against_each_other():
"""View-space coordinates of two different views mean nothing together."""
findings, _u, _r = ticketpage.callout_overlap_status(
[_note('Elevation Front', 'a', (0, 0, 1, 1)),
_note('Elevation Top', 'b', (0, 0, 1, 1))])
assert findings[0][0] == 'OK'
def test_15_rotated_text_is_set_aside_not_flagged():
"""An axis-aligned box on rotated text overstates the ink by the length
of the string, so comparing it manufactures overlaps that do not print."""
findings, _u, rotated = ticketpage.callout_overlap_status(
[_note('V', 'straight', (0, 0, 1, 1)),
_note('V', 'turned', (0.5, 0.5, 1.5, 1.5), rotated=True)])
assert rotated == ['text note "turned"']
assert findings[0][0] == 'OK'
def test_15_an_unreadable_box_is_could_not_judge_and_is_named():
findings, unread, _r = ticketpage.callout_overlap_status(
[_note('V', 'readable', (0, 0, 1, 1)), _note('V', 'no box', None)])
assert unread == ['text note "no box"']
assert findings[0][0] == 'OK'
def test_15_a_rounding_sliver_is_not_an_overlap():
"""A thousandth of an inch is the box's own slop, not two notes touching."""
findings, _u, _r = ticketpage.callout_overlap_status(
[_note('V', 'a', (0, 0, 1, 1)), _note('V', 'b', (0.999, 0, 2, 1))])
assert findings[0][0] == 'OK'
def test_15_says_nothing_at_all_when_there_is_nothing_to_compare():
assert ticketpage.callout_overlap_status([]) == ([], [], [])
# =========================================== 5.6 bar marks unique (computed)
def test_56_one_mark_naming_two_parts_warns():
"""The expensive one: a mark is what a fabricator cuts to."""
findings, _u = ticketpage.bar_mark_uniqueness_status(
[{'text': 'SR310 (E.L.)', 'view': 'Elevation Front'}],
[{'mark': 'SR310', 'family': 'A', 'length_ft': 11.0},
{'mark': 'SR310', 'family': 'A', 'length_ft': 4.0}])
assert findings[0][0] == 'WARN'
assert 'name more than one part' in findings[0][1]
assert 'SR310' in findings[0][1]
def test_56_two_instances_of_the_same_part_are_not_two_populations():
findings, _u = ticketpage.bar_mark_uniqueness_status(
[{'text': 'SR310 (E.L.)', 'view': 'V'}],
[{'mark': 'SR310', 'family': 'A', 'length_ft': 11.0},
{'mark': 'SR310', 'family': 'A', 'length_ft': 11.0}])
assert findings[0][0] == 'OK'
def test_56_a_printed_mark_no_bar_carries_warns():
findings, _u = ticketpage.bar_mark_uniqueness_status(
[{'text': 'SR310 (E.L.)', 'view': 'V'},
{'text': 'SR999 (N.F.)', 'view': 'V'}],
[{'mark': 'SR310', 'family': 'A', 'length_ft': 11.0}])
warns = [m for (s, m) in findings if s == 'WARN']
assert any('match no bar in the model' in m and 'SR999' in m
for m in warns)
def test_56_a_modelled_bar_never_printed_warns():
findings, _u = ticketpage.bar_mark_uniqueness_status(
[{'text': 'SR310 (E.L.)', 'view': 'V'}],
[{'mark': 'SR310', 'family': 'A', 'length_ft': 11.0},
{'mark': 'SR777', 'family': 'A', 'length_ft': 3.0}])
warns = [m for (s, m) in findings if s == 'WARN']
assert any('never printed' in m and 'SR777' in m for m in warns)
def test_56_the_printed_string_contains_the_mark_it_never_equals_it():
"""Measured on the live model: `SR310 (E.L.)`, not `SR310`. A check that
compared strings for equality would call every printed mark an orphan."""
findings, _u = ticketpage.bar_mark_uniqueness_status(
[{'text': 'SR324 (F.F.)', 'view': 'V'}],
[{'mark': 'SR324', 'family': 'A', 'length_ft': 9.0}])
assert findings[0][0] == 'OK'
def test_56_a_bar_with_no_mark_is_could_not_judge_never_a_missing_print():
"""An absent mark cannot be looked for in the printed text."""
findings, unread = ticketpage.bar_mark_uniqueness_status(
[{'text': 'SR310 (E.L.)', 'view': 'V'}],
[{'mark': 'SR310', 'family': 'A', 'length_ft': 11.0},
{'mark': None, 'family': 'BAR_FAMILY_B', 'length_ft': 2.0}])
assert unread == ['BAR_FAMILY_B']
assert findings[0][0] == 'OK'
def test_56_mark_tokens_reads_the_shapes_the_plant_actually_prints():
assert ticketpage.mark_tokens('SR310 (E.L.)') == ['SR310']
assert ticketpage.mark_tokens('P76 W/ SR5602 (S.I.F.)') == ['P76', 'SR5602']
assert ticketpage.mark_tokens('P106 (S.I.F.)(E.S.)') == ['P106']
assert ticketpage.mark_tokens('') == []
assert ticketpage.mark_tokens(None) == []
def test_56_a_mark_token_is_whole_never_a_prefix_of_a_longer_one():
"""`SR1` inside `SR1101` must not become a mark of its own."""
assert ticketpage.mark_tokens('SR1101 (N.F.)') == ['SR1101']
# ------------------------------------------ the split of 5.5, in the table
def test_the_55_split_left_legibility_with_a_person_and_moved_uniqueness():
rows = dict((i[0], i) for i in ticketpage.CHECKLIST_ITEMS)
assert rows['5.5'][1] == 'Bar marks legible on the drawing'
assert rows['5.5'][2] == 'D'
assert rows['5.5'][5] is None # no check: a person reads it
assert '5.5' in ticketpage.CHECKLIST_FOR_ENGINEER
assert 'unique' in rows['5.6'][1]
assert rows['5.6'][5] == 'check_bar_mark_uniqueness'
assert '5.6' not in ticketpage.CHECKLIST_FOR_ENGINEER
def test_the_three_new_rows_are_owned_by_the_codes_their_checks_emit():
owners = dict((i[0], i[3]) for i in ticketpage.CHECKLIST_ITEMS)
assert owners['1.5'] == ('CALLOUT-OVERLAP',)
assert owners['5.6'] == ('BARMARK-UNIQUE',)
assert owners['5.7'] == ('BAR-LENGTH',)
def test_the_could_not_judge_codes_can_never_decide_a_row():
"""BAR-LENGTH-UNREAD and friends are OK-severity and UNOWNED on purpose:
they must be visible without masquerading as a verdict."""
every_owner = set()
for item in ticketpage.CHECKLIST_ITEMS:
every_owner.update(item[3] or ())
for code in ('BAR-LENGTH-UNREAD', 'BARMARK-UNREAD',
'CALLOUT-OVERLAP-UNREAD'):
assert code not in every_owner
# ============================================== 4.3 jacking force, in units
#
# `tolerances.it_lb` was declared, documented and offered in the config form
# -- and read by nothing, while the check compared kips against a hardcoded
# `0.1`. Wiring it is a units question before it is anything else: the key
# holds POUNDS, the ticket prints KIPS, and a factor of 1000 dropped here is
# a check a thousand times stricter that fails on correct tickets.
#
# The owner's ruling (2026-08-30) was 100 lb -- the same 0.1 kips the literal
# used -- so the point of the change is the knob, not the number.
def _old_verdict(printed_kips, design_it_lb):
"""The comparison exactly as the deleted literal did it."""
return abs(printed_kips - design_it_lb / 1000.0) > 0.1
def test_43_at_100_lb_every_verdict_is_the_one_the_old_literal_gave():
"""The behaviour did not move. 100 lb IS 0.1 kips, across the whole band
either side of the boundary."""
for design_lb in (28920.0, 31000.0, 20250.0):
for delta_lb in (0, 1, 50, 99, 101, 150, 900, 5000):
for sign in (1, -1):
printed_kips = (design_lb + sign * delta_lb) / 1000.0
sev, _msg = ticketpage.jacking_force_status(
printed_kips, design_lb, 100.0)
was_warn = _old_verdict(printed_kips, design_lb)
assert (sev == 'WARN') == was_warn, (design_lb, delta_lb, sign)
def test_43_the_one_case_that_did_move_is_exactly_on_the_boundary():
"""EXACTLY 100 lb apart, and only there. `28.82 - 28.92` in binary
floats is 0.10000000000000142, so the old kips comparison called a gap
of exactly the tolerance a WARN on some values and an OK on others,
depending on the numbers. Working in whole pounds removes the rounding
and the boundary is now inclusive everywhere, which is what `within
tolerance` has always meant. Said out loud rather than left as a
surprise: it is the only difference between the two."""
# the same gap of exactly 100 lb, judged two different ways
assert _old_verdict(28.82, 28920.0) is True # 100 lb low -> WARN
assert _old_verdict(29.02, 28920.0) is False # 100 lb high -> OK
for design_lb in (28920.0, 31000.0, 20250.0):
for sign in (1, -1):
printed_kips = (design_lb + sign * 100) / 1000.0
sev, _msg = ticketpage.jacking_force_status(
printed_kips, design_lb, 100.0)
assert sev == 'OK', (design_lb, sign)
def test_43_the_real_ticket_still_passes():
"""The measured case: the note prints 28.9 kips, the design carries
28,920 lb. 20 lb apart, and it read OK before the key was wired."""
sev, msg = ticketpage.jacking_force_status(28.9, 28920, 100.0)
assert sev == 'OK'
assert '28.9 kips' in msg
# the sentence names BOTH units and the key, so nobody has to guess
# which of the two numbers the tolerance is in
assert '20 lb apart' in msg
assert '100 lb' in msg
assert 'tolerances.it_lb' in msg
def test_43_the_tolerance_is_pounds_and_the_print_is_kips():
"""A 1 lb tolerance -- what the key DECLARED while nothing read it --
is a hundred times stricter, and that is the whole reason the value
went to the owner instead of the key being wired as written."""
assert ticketpage.jacking_force_status(28.9, 28920, 100.0)[0] == 'OK'
assert ticketpage.jacking_force_status(28.9, 28920, 1.0)[0] == 'WARN'
# and a plant that wants it looser can have that too: the knob turns
# both ways, which is what wiring the key bought
assert ticketpage.jacking_force_status(28.4, 28920, 100.0)[0] == 'WARN'
assert ticketpage.jacking_force_status(28.4, 28920, 600.0)[0] == 'OK'
def test_43_a_value_that_is_not_a_number_is_not_run_and_never_a_pass():
for args in ((None, 28920, 100.0), ('n/a', 28920, 100.0),
(28.9, 'lots', 100.0), (28.9, 28920, 'tight')):
sev, msg = ticketpage.jacking_force_status(*args)
assert sev == 'NOT RUN', args
assert msg
# ========================== 5.1's detail-sheet half, and what it cannot do
#
# `nomenclature.detail_sheet_prefix` -- the plant naming the series its
# detail sheets are numbered from -- was declared and read by nothing,
# because 5.1's detail-sheet half was never built. The owner ruled: read it.
#
# What it buys is the ability to recognise a detail BUBBLE (a detail number
# over a sheet name) on the BED PLAN and tell it from a part call-out. What
# it cannot buy is the CONTENTS of that sheet, which nothing here opens --
# so a plan that cites a detail leaves 5.1 unanswered, out loud.
def test_51_a_bubble_needs_the_plants_prefix_to_be_recognised_at_all():
"""No prefix is could-not-judge (None), not 'no details' ([])."""
assert ticketpage.detail_sheet_refs('06/X1', None) is None
assert ticketpage.detail_sheet_refs('06/X1', '') is None
assert ticketpage.detail_sheet_refs('06/X1', 'X') == ['06/X1']
def test_51_a_bubble_is_found_and_a_part_callout_is_not():
# the dotted sheet name is deliberate: a plant numbers its detail
# series how it likes, and the prefix is all this may assume
text = 'P76 W/ SR5602 (S.I.F.)\n06/X2.1\nSP126 (S.I.F.)\n12 / X1'
assert ticketpage.detail_sheet_refs(text, 'X') == ['06/X2.1', '12/X1']
# a bare part call-out is never mistaken for a bubble
assert ticketpage.detail_sheet_refs(
'P106 (S.I.F.)(E.S.)\nSR309 (E.L.)', 'X') == []
assert ticketpage.detail_sheets_of(['06/X2.1', '12/X1', '07/X2.1']) == \
['X1', 'X2.1']
def test_51_a_plan_citing_no_detail_is_a_verdict_not_a_silence():
"""The one pass this half has, and it is a real one: nothing on the
piece reaches the model through a detail sheet."""
findings, reason = ticketpage.detail_callout_status(
'P106 (S.I.F.)\nSR309 (E.L.)', 'X')
assert reason is None
assert [f[0] for f in findings] == ['OK']
assert 'no X-sheet detail' in findings[0][1]
def test_51_a_cited_detail_is_not_run_and_names_the_sheet():
findings, reason = ticketpage.detail_callout_status(
'P106 (S.I.F.)\n06/X1', 'X')
assert findings == []
assert reason
assert '06/X1' in reason and 'X1' in reason
assert 'nothing reads' in reason
def test_51_an_undeclared_prefix_names_the_key_it_is_missing():
findings, reason = ticketpage.detail_callout_status('06/X1', None)
assert findings == []
assert 'nomenclature.detail_sheet_prefix' in reason
def test_51_an_unreadable_bed_plan_is_its_own_reason():
findings, reason = ticketpage.detail_callout_status(None, 'X')
assert findings == []
assert 'BED PLAN could not be read' in reason
# ------------------------- a row that names two checks is answered by both
def test_51_names_both_halves_and_both_codes():
rows = dict((i[0], i) for i in ticketpage.CHECKLIST_ITEMS)
assert rows['5.1'][3] == ('BOM-MEMBERS', 'DETAIL-CALLOUT')
assert rows['5.1'][5] == ('check_bom_members', 'check_detail_callouts')
def test_a_row_with_two_checks_is_ok_only_when_both_of_them_ran():
log = [('check_bom_members', 'RAN', None),
('check_detail_callouts', 'RAN', None)]
rows = dict((r[0], r) for r in ticketpage.checklist_states([], log))
assert rows['5.1'][3] == 'OK'
def test_a_quiet_half_makes_the_row_not_run_however_green_the_other_is():
"""The silent pass this table exists to prevent: 5.1's BOM half passing
while the half that could still fail it was never asked."""
log = [('check_bom_members', 'RAN', None),
('check_detail_callouts', 'SKIPPED',
'the BED PLAN reaches 1 part(s) through a detail bubble')]
findings = [_F('OK', 'BOM-MEMBERS')]
rows = dict((r[0], r) for r in ticketpage.checklist_states(findings, log))
assert rows['5.1'][3] == 'NOT RUN'
assert 'detail bubble' in rows['5.1'][4]
assert ticketpage.state_color(rows['5.1'][3]) == 'red'
def test_both_halves_quiet_reports_both_reasons_and_cuts_neither():
log = [('check_bom_members', 'SKIPPED', 'this ticket has no assembly'),
('check_detail_callouts', 'SKIPPED', 'no detail_sheet_prefix')]
rows = dict((r[0], r) for r in ticketpage.checklist_states([], log))
assert rows['5.1'][3] == 'NOT RUN'
assert 'no assembly' in rows['5.1'][4]
assert 'no detail_sheet_prefix' in rows['5.1'][4]
def test_a_check_that_never_ran_at_all_is_not_a_pass_either():
"""Absent from the log is not RAN. A row whose second half never ran
must not read OK off the first half."""
log = [('check_bom_members', 'RAN', None)]
rows = dict((r[0], r) for r in ticketpage.checklist_states([], log))
assert rows['5.1'][3] == 'NOT RUN'
def test_the_one_check_rows_are_untouched_by_the_tuple_support():
log = [('check_lifter_capacity', 'RAN', None)]
rows = dict((r[0], r) for r in ticketpage.checklist_states([], log))
assert rows['6.1'][3] == 'OK'
log = [('check_lifter_capacity', 'SKIPPED', 'no lifters modelled')]
rows = dict((r[0], r) for r in ticketpage.checklist_states([], log))
assert rows['6.1'][3] == 'NOT RUN'
assert rows['6.1'][4] == 'no lifters modelled'
# ============================================ 6.6: parity and a bed note
#
# Owner's ruling, 2026-08-31: when the count of one part on a piece is ODD,
# the ticket's BED PLAN must carry a particular phrase.
#
# THE PART NUMBER BELOW IS INVENTED, and that is the point: a test that
# asserted on the plant's real part would pin the plant's rule into the
# repository from the test side, which is the same defect as writing it
# into the check. Every assertion here is about the SHAPE of the answer,
# and not one of them would change if the plant changed its hardware.
#
# The WORDING is the real one, deliberately, and it is doing a job: the
# sibling guard in `test_ticketchecks_host.py` asserts that this exact
# string appears in neither module, so the day somebody hardcodes it the
# suite says so. It identifies nobody -- it is an English drafting note.
PART = ('ACME_PART_9',)
PHRASE = 'FOR ERECTION ONLY'
def _parity(count, unknown=None, bed_text=None, parity='odd',
phrases=(PHRASE,), names=PART, verified=True, **kw):
return ticketpage.parity_note_status(
count, list(unknown or []), bed_text, list(names), parity,
list(phrases), verified=verified, **kw)
def test_66_odd_with_the_note_is_ok():
findings, reason = _parity(3, bed_text='NOTES: FOR ERECTION ONLY.')
assert reason is None
assert [f[0] for f in findings] == ['OK']
assert 'odd' in findings[0][1]
def test_66_odd_without_the_note_is_the_defect():
findings, reason = _parity(3, bed_text='LIFT WITH CARE')
assert reason is None
assert findings[0][0] == 'FAIL'
assert 'must carry' in findings[0][1]
def test_66_even_is_ok_and_says_the_rule_does_not_apply():
"""The BED PLAN is not even read: an even count answers the question
on its own, and demanding a readable plan here would print NOT RUN for
a piece that was fully judged."""
findings, reason = _parity(4, bed_text=None)
assert reason is None
assert findings[0][0] == 'OK'
assert 'does not apply' in findings[0][1]
def test_66_zero_is_a_judged_ok_and_not_a_could_not_judge():
"""Zero is even, so the rule does not apply -- a real answer. The
member total is printed with it so a family name that matches nothing
is visible rather than silently even."""
findings, reason = _parity(0, bed_text=None, total=112)
assert reason is None
assert findings[0][0] == 'OK'
assert '112' in findings[0][1]
assert 'Judged' in findings[0][1]
def test_66_zero_and_unrecognised_do_not_collapse_into_each_other():
zero, _ = _parity(0, total=112)
silent, reason = _parity(0, unknown=[{'family': '', 'why': 'no name'}],
total=112)
assert zero and zero[0][0] == 'OK'
assert silent == [] and reason
def test_66_one_unread_member_stands_the_whole_count_down():
"""One member flips an odd count to even, so a partial census is not a
slightly worse answer, it is the opposite answer half the time."""
findings, reason = _parity(
3, unknown=[{'family': '', 'why': 'a member carries no family name'}],
bed_text=PHRASE)
assert findings == []
assert 'not trustworthy' in reason
assert 'no family name' in reason
def test_66_unreadable_bed_plan_is_not_run_with_its_own_reason():
findings, reason = _parity(3, bed_text=None,
bed_reason='no BED PLAN view on this ticket')
assert findings == []
assert reason == 'no BED PLAN view on this ticket'
def test_66_no_phrase_declared_is_not_run_naming_the_key():
findings, reason = _parity(3, phrases=(), bed_text=PHRASE)
assert findings == []
assert 'require_text' in reason
def test_66_no_part_declared_is_not_run_naming_the_key():
findings, reason = _parity(3, names=(), bed_text=PHRASE)
assert findings == []
assert 'family_names' in reason
def test_66_no_parity_declared_is_not_run_naming_the_key():
findings, reason = _parity(3, parity=None, bed_text=PHRASE)
assert findings == []
assert 'parity' in reason
def test_66_an_unverified_family_name_stands_down_and_quotes_the_reason():
"""The guard `reinforcing_rule.parameter_verified` carries, for the same
reason: a family name nobody has confirmed matches nothing, counts zero
on every piece, and zero is even -- a permanent invisible OK."""
findings, reason = _parity(3, verified=False, bed_text=PHRASE,
unverified_because='nobody has read it live')
assert findings == []
assert 'family_verified' in reason
assert 'nobody has read it live' in reason
def test_66_an_unmakeable_census_reports_its_own_reason():
findings, reason = _parity(None, gather_reason='no assembly on this '
'ticket', bed_text=PHRASE)
assert findings == []
assert reason == 'no assembly on this ticket'
def test_66_a_count_of_none_with_no_reason_is_never_a_pass():
findings, reason = _parity(None, bed_text=PHRASE)
assert findings == []
assert 'not run' in reason
def test_66_phrase_match_is_case_insensitive_and_survives_line_breaks():
for text in ('for erection only', 'FOR\nERECTION\nONLY',
'For Erection Only', 'NOTE 4: (FOR ERECTION ONLY)'):
findings, reason = _parity(1, bed_text=text)
assert reason is None
assert findings[0][0] == 'OK', text
def test_66_an_empty_bed_plan_is_a_fail_not_a_silence():
"""A view that was READ and holds no text does not carry the phrase.
That is a verdict about the ticket; only an unreadable view is silence."""
findings, reason = _parity(1, bed_text='')
assert reason is None
assert findings[0][0] == 'FAIL'
def test_66_the_even_plant_is_a_plant_and_not_a_bug():
"""Which parity demands the note is the plant's, so the mirror rule has
to work: an even count without the note fails, an odd one passes."""
findings, _ = _parity(4, parity='even', bed_text='NOTHING HERE')
assert findings[0][0] == 'FAIL'
findings, _ = _parity(3, parity='even', bed_text=None)
assert findings[0][0] == 'OK'
def test_66_a_present_phrase_never_claims_the_note_is_about_this_part():
findings, _ = _parity(1, bed_text=PHRASE)
assert 'not claimed' in findings[0][1]
def test_66_several_acceptable_wordings_are_allowed():
findings, _ = _parity(1, phrases=('ERECTION USE ONLY', PHRASE),
bed_text='... FOR ERECTION ONLY ...')
assert findings[0][0] == 'OK'
def test_66_squash_phrase_keeps_none_distinct_from_empty():
assert ticketpage.squash_phrase(None) is None
assert ticketpage.squash_phrase(' ') == ''
def test_66_row_exists_and_names_its_check_and_its_code():
row = dict((r[0], r) for r in ticketpage.CHECKLIST_ITEMS)['6.6']
assert row[3] == ('PARITY-NOTE',)
assert row[5] == 'check_parity_note'
assert '6.6' not in ticketpage.CHECKLIST_FOR_ENGINEER
def test_66_a_skipped_check_makes_the_row_not_run_with_the_reason():
log = [('check_parity_note', 'SKIPPED', 'no family_names declared')]
rows = dict((r[0], r) for r in ticketpage.checklist_states([], log))
assert rows['6.6'][3] == 'NOT RUN'
assert rows['6.6'][4] == 'no family_names declared'
assert ticketpage.state_color(rows['6.6'][3]) == 'red'
def test_66_a_deferred_bed_plan_reader_is_called_only_when_needed():
"""The BED PLAN may arrive as a callable returning (text, reason).
Walking every sheet's placed views is not free, and an even count never
needs it -- so the gate order has to be observable, not just intended."""
calls = []
def reader():
calls.append(True)
return 'NOTE: FOR ERECTION ONLY', None
findings, reason = _parity(4, bed_text=reader)
assert findings[0][0] == 'OK' and reason is None
assert calls == []
findings, reason = _parity(3, bed_text=reader)
assert findings[0][0] == 'OK' and reason is None
assert calls == [True]
def test_66_a_deferred_reader_may_hand_back_the_reason_it_found():
def reader():
return None, 'no view of that name is placed on this ticket'
findings, reason = _parity(3, bed_text=reader)
assert findings == []
assert reason == 'no view of that name is placed on this ticket'
# ============================================================================
# THE FIFTH STATE: a question that is not about this kind of piece
# ============================================================================
def _off(**over):
entry = {'not_applicable': True, 'condition': 'insulated',
'reason': 'these have no release requirement',
'since': '2026-08-31', 'declared_by': 'Will',
'layer': 'plant', 'unaskable': False}
entry.update(over)
return entry
def test_not_applicable_has_its_own_colour_and_not_a_reused_one():
"""A new state needs its own entry, never a reuse.
Grey already means "it ran and this job asked for it quietly". A reader
who has to work out which kind of quiet a grey row is has stopped
trusting the table, which is the whole asset here.
"""
colour = ticketpage.state_color(ticketpage.NOT_APPLICABLE_STATE)
assert colour == 'blue'
for other in ('OK', 'WARN', 'FOR ENGINEER', 'FAIL', 'NOT RUN',
ticketpage.MUTED_STATE):
assert colour != ticketpage.state_color(other), other
assert colour in ticketpage.STATE_COLORS
def test_the_existing_colours_are_untouched_by_the_new_one():
assert ticketpage.state_color('OK') == 'green'
assert ticketpage.state_color('WARN') == 'yellow'
assert ticketpage.state_color('FOR ENGINEER') == 'yellow'
assert ticketpage.state_color('NOT RUN') == 'red'
assert ticketpage.state_color('FAIL') == 'red'
assert ticketpage.state_color(ticketpage.MUTED_STATE) == 'grey'
assert ticketpage.state_color('WHATEVER') == 'red'
def test_not_applicable_is_not_judged_and_has_its_own_machine_key():
assert ticketpage.state_key(ticketpage.NOT_APPLICABLE_STATE) == \
'not_applicable'
assert ticketpage.state_is_judged(ticketpage.NOT_APPLICABLE_STATE) is False
keys = [k for (_s, k, _j) in ticketpage.STATE_VOCABULARY]
assert len(keys) == len(set(keys))
def test_a_switched_off_row_is_repainted_and_keeps_the_declaration():
rows = [('3.3', "f'ci/f'c release ratio in band", 'B', 'NOT RUN', 'why')]
kept, notes = ticketpage.apply_applicability(rows, {'3.3': _off()})
assert kept[0][3] == ticketpage.NOT_APPLICABLE_STATE
note = kept[0][4]
assert 'these have no release requirement' in note
assert '2026-08-31' in note
assert 'insulated' in note
assert 'plant' in note
assert notes[0][0] == '3.3'
def test_a_row_that_failed_is_never_switched_off():
"""The honesty invariant, in the one place it can actually bite.
Two rows can name one check (4.4 and 4.6 both name check_strand_layout),
so a check kept alive for the row that still applies can put a finding on
the row that does not. Silencing a FAIL that already exists is the one
thing no switch in this module may do.
"""
rows = [('4.4', 'Strand positions', 'B', 'FAIL', 'off by 3 in')]
kept, notes = ticketpage.apply_applicability(rows, {'4.4': _off()})
assert kept[0][3] == 'FAIL'
assert kept[0][4] == 'off by 3 in'
assert 'DECLINED' in notes[0][7]
def test_a_row_with_no_declaration_is_untouched():
rows = [('3.1', "f'c", 'A', 'OK', None)]
kept, notes = ticketpage.apply_applicability(rows, {})
assert kept == rows
assert notes == []
def test_a_scoped_row_on_an_unclassifiable_piece_is_red_not_blue():
"""NOT RUN, never NOT APPLICABLE and never OK.
Blue would claim the piece is not that type -- exactly what could not be
established. Red says nobody could ask it, which is true, and it is cured
by making the classification work rather than by anybody's ruling.
"""
rows = [('6.6', 'Odd part count', 'B', 'OK', None)]
kept, _notes = ticketpage.apply_applicability(rows, {'6.6': {
'not_applicable': False, 'unaskable': True,
'reason': 'this row is about insulated pieces only and this piece '
'could not be classified. It was NOT ASKED.'}})
assert kept[0][3] == 'NOT RUN'
assert ticketpage.state_color(kept[0][3]) == 'red'
assert 'NOT ASKED' in kept[0][4]
def test_muting_never_repaints_a_row_that_is_already_not_applicable():
"""Two mechanisms doing one job, said out loud rather than layered.
Grey would replace "this question is not about this piece" with "this job
asked for it quietly", which is a weaker and different claim.
"""
rows = [('3.3', 'release ratio', 'B',
ticketpage.NOT_APPLICABLE_STATE, 'declared')]
kept, notes = ticketpage.apply_muting(rows, {'3.3': 'muted too'})
assert kept[0][3] == ticketpage.NOT_APPLICABLE_STATE
assert 'THE MUTE DID NOTHING' in notes[0][3]
assert ticketpage.muting_problem(notes)
def test_the_headline_gives_the_new_state_its_own_bucket():
rows = [('3.1', 'a', 'A', 'OK', None),
('3.3', 'b', 'B', ticketpage.NOT_APPLICABLE_STATE, 'declared')]
counts = ticketpage.checklist_tally(rows)
assert counts[ticketpage.NOT_APPLICABLE_STATE] == 1
headline = ticketpage.checklist_headline(counts)
assert '1 ' + ticketpage.NOT_APPLICABLE_STATE in headline
assert '1 OK' in headline
assert headline.index('OK') < headline.index(
ticketpage.NOT_APPLICABLE_STATE)
def test_the_colour_key_explains_the_new_state_on_the_page():
key = ticketpage.checklist_key_html()
assert ticketpage.NOT_APPLICABLE_STATE in key
assert 'not run' in key
assert ticketpage.STATE_COLORS['blue'][0] in key
def test_the_section_opens_itself_whenever_anything_was_switched_off():
"""Unlike muting, which folds when it is tidy.
A mute leaves the check running; this stops it. A mistaken rule here is a
safety check disabled in silence, so the page says so every run.
"""
notes = [('3.3', 'release ratio', 'why', 'insulated', '2026-08-31',
'Will', 'plant', None)]
assert ticketpage.applicability_problem(notes)
assert ticketpage.applicability_problem([], []) is None
assert 'REFUSED' in ticketpage.applicability_problem(
[], [('3.3', 'the entry has no date')])
def test_every_switched_off_line_names_the_layer_that_declared_it():
notes = [('3.3', 'release ratio', 'why', 'insulated', '2026-08-31',
'Will', 'plant', None)]
line = ticketpage.applicability_lines(notes)[0]
assert 'plant' in line
assert 'why' in line
unknown = ticketpage.applicability_lines(
[('3.3', 'r', 'why', 'any', '2026-08-31', None, None, None)])[0]
assert 'UNKNOWN' in unknown
def test_the_profile_line_names_the_classification_and_the_rows():
line = ticketpage.classification_line(
'PNL101', {'panel_type': 'insulated', 'verified': True,
'why': 'two signals agree'},
[('3.3', 'release ratio', 'why', 'insulated', '2026-08-31', 'Will',
'plant', None)])
assert 'PNL101' in line and 'insulated' in line
assert '3.3' in line
assert 'two signals agree' in line
def test_the_profile_line_says_when_a_classification_was_not_used():
line = ticketpage.classification_line(
'PNL101', {'panel_type': 'insulated', 'verified': False,
'why': 'two signals agree'}, [])
assert 'detect_verified' in line
assert 'no row was declared not applicable' in line
def test_the_profile_line_says_the_full_checklist_ran_when_unclassified():
line = ticketpage.classification_line(
'PNL101', {'panel_type': None, 'verified': True, 'why': 'nothing'}, [])
assert 'NOT CLASSIFIED' in line
assert 'FULL checklist' in line
def test_applicability_decisions_covers_every_row_and_never_a_subset():
"""An absence is not a value here either. A caller reading this as "the
rows in here are the switched-off ones" would be inferring from a gap."""
calls = []
def _resolver(know, item, panel_type=None, verified=False):
calls.append(item)
return {'not_applicable': False}
got = ticketpage.applicability_decisions({}, resolver=_resolver)
assert sorted(got) == sorted([i[0] for i in ticketpage.CHECKLIST_ITEMS])
assert len(calls) == len(ticketpage.CHECKLIST_ITEMS)
assert ticketpage.switched_off(got) == []
# ============================================================================
# 5.6: A BAR THAT SHIPS INSIDE ANOTHER PART, AND A PART THAT IS NOT A BAR
# ============================================================================
#
# Owner, 2026-08-31: "SR4996 是 SP29/30 的附属 rebar,我记得我增加过这条知识了,
# 为什么还在 WP117 报 warn?" He had recorded it -- three times over -- and this
# check read none of it. The fixture below is the WP115/WP117 shape.
WP115_PRINTED = [
{'text': 'P78 W/ SR5502 (S.I.F.) (E.S.)', 'view': 'Detail 1'},
{'text': 'P114 (F.F.)', 'view': 'Detail 1'},
{'text': 'SP29 (N.F.)', 'view': 'Detail 1'},
{'text': 'SP30 (N.F.)', 'view': 'Detail 1'},
{'text': 'SP31 (N.F.)', 'view': 'Detail 1'},
{'text': 'SR310 (E.L.)', 'view': 'Detail 2'},
]
WP115_BARS = [
{'mark': 'SR310', 'family': 'REBAR', 'length_ft': 8.0},
{'mark': 'SR5502', 'family': 'REBAR', 'length_ft': 4.0},
{'mark': 'SR4996', 'family': 'REBAR', 'length_ft': 1.5},
]
WP115_ATTACHED = {'SR4996': ['SP29', 'SP30']}
def _grammar(**kinds):
"""A `classify` callable over a tiny declared grammar."""
def classify(token):
for prefix in sorted(kinds, key=len, reverse=True):
if token.upper().startswith(prefix):
return kinds[prefix], 'declared {0}'.format(prefix)
return 'unknown', 'no declared prefix matches {0}'.format(token)
return classify
WP115_GRAMMAR = _grammar(P='hardware', SP='hardware', SR='bar_mark')
def test_a_covered_bar_whose_parent_is_printed_is_accounted_for():
"""The owner's complaint, as a test.
SR4996 is never drawn on its own -- it ships inside the SP29/SP30 angles
and their callout covers it. The plant file says so in as many words, and
5.6 reported it as never printed anyway.
"""
findings, unread = ticketpage.bar_mark_uniqueness_status(
WP115_PRINTED, WP115_BARS,
attached=WP115_ATTACHED, classify=WP115_GRAMMAR)
assert [sev for (sev, _m) in findings] == ['OK']
message = findings[0][1]
assert 'SR4996 is attached to SP29/SP30' in message
assert unread == []
def test_the_coverage_is_named_and_never_a_silent_exclusion():
"""A covered bar is a JUDGED outcome. A coverage nobody can see on the
report is a coverage nobody can question."""
findings, _unread = ticketpage.bar_mark_uniqueness_status(
WP115_PRINTED, WP115_BARS,
attached=WP115_ATTACHED, classify=WP115_GRAMMAR)
message = findings[0][1]
assert 'never drawn on their own' in message
assert 'parent item callout covers them' in message
def test_a_covered_bar_whose_parent_is_also_unprinted_is_still_a_finding():
"""And the finding is about the PARENT.
The bar is unaccounted for either way; what changed is whose callout is
missing. A report that named the bar would send the reader to fix the
wrong thing.
"""
printed = [row for row in WP115_PRINTED
if not row['text'].startswith(('SP29', 'SP30'))]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, WP115_BARS,
attached=WP115_ATTACHED, classify=WP115_GRAMMAR)
warns = [m for (sev, m) in findings if sev == 'WARN']
assert len(warns) == 1
assert 'PARENT callout' in warns[0]
assert 'SR4996 is attached to SP29/SP30' in warns[0]
# ... and it is NOT reported as an ordinary never-printed bar.
assert 'are never printed on this ticket' not in warns[0]
def test_hardware_part_numbers_are_not_candidate_bar_marks():
"""The WP115 half of the same run: P78, P114, SP29, SP30 and SP31 were
reported as printed bar marks matching no bar in the model. They match no
bar because they are hardware."""
findings, _unread = ticketpage.bar_mark_uniqueness_status(
WP115_PRINTED, WP115_BARS,
attached=WP115_ATTACHED, classify=WP115_GRAMMAR)
blob = ' '.join(m for (_s, m) in findings)
assert 'match no bar in the model' not in blob
for part in ('P78', 'P114', 'SP29', 'SP30', 'SP31'):
assert part in blob # set aside OUT LOUD, not dropped
assert 'hardware part numbers' in blob
def test_the_w_slash_form_carries_both_populations():
"""`P78 W/ SR5502 (S.I.F.)` is a part AND a bar. The part is set aside
and the bar after it still counts -- and no `W/` splitter is needed,
because the grammar answers per token."""
printed = [{'text': 'P78 W/ SR5502 (S.I.F.) (E.S.)', 'view': 'Detail 1'}]
bars = [{'mark': 'SR5502', 'family': 'REBAR', 'length_ft': 4.0}]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, bars, classify=WP115_GRAMMAR)
assert [sev for (sev, _m) in findings] == ['OK']
assert 'P78' in findings[0][1]
# The bar half really is being compared: take it out of the model and the
# row has something to say about it.
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, [{'mark': 'SR310', 'family': 'REBAR', 'length_ft': 8.0}],
classify=WP115_GRAMMAR)
warns = ' '.join(m for (sev, m) in findings if sev == 'WARN')
assert 'SR5502' in warns
assert 'P78' not in warns
def test_a_token_the_grammar_cannot_place_stays_a_finding():
"""Could-not-judge, never a quiet drop. An unplaceable token is a fact
about the sheet or about the grammar, and it is reported as one."""
printed = [{'text': 'MK12 (F.F.)', 'view': 'Detail 1'},
{'text': 'SR310 (E.L.)', 'view': 'Detail 2'}]
bars = [{'mark': 'SR310', 'family': 'REBAR', 'length_ft': 8.0}]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, bars, classify=WP115_GRAMMAR)
warns = [m for (sev, m) in findings if sev == 'WARN']
assert len(warns) == 1
assert 'could not be placed' in warns[0]
assert 'MK12' in warns[0]
def test_what_was_set_aside_is_said_even_when_the_row_is_not_green():
"""A narrowing visible only on a green row is a narrowing nobody audits
on the runs that matter."""
bars = WP115_BARS + [{'mark': 'SR999', 'family': 'REBAR',
'length_ft': 3.0}]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
WP115_PRINTED, bars,
attached=WP115_ATTACHED, classify=WP115_GRAMMAR)
sevs = [sev for (sev, _m) in findings]
assert 'WARN' in sevs and 'OK' in sevs
aside = [m for (sev, m) in findings if sev == 'OK'][0]
assert 'what 5.6 set aside' in aside
assert 'SR4996' in aside and 'P78' in aside
def test_a_bar_mark_the_grammar_misfiled_is_still_ink_on_the_sheet():
"""(c) asks "is this mark printed anywhere", and it answers from EVERY
token, not from the narrowed candidate set. Otherwise a grammar mistake
would turn a printed bar into a never-printed one -- a finding invented
by the narrowing itself."""
printed = [{'text': 'SR310 (E.L.)', 'view': 'Detail 1'}]
bars = [{'mark': 'SR310', 'family': 'REBAR', 'length_ft': 8.0}]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, bars, classify=_grammar(SR='hardware'))
assert [sev for (sev, _m) in findings] == ['OK']
assert 'never printed' not in findings[0][1]
def test_with_no_knowledge_at_all_the_check_behaves_exactly_as_before():
"""Both narrowings default to off. A plant that has declared neither key
gets the comparison it had before either existed -- noisy, never quiet."""
findings, _unread = ticketpage.bar_mark_uniqueness_status(
WP115_PRINTED, WP115_BARS)
warns = ' '.join(m for (sev, m) in findings if sev == 'WARN')
assert 'match no bar in the model' in warns
assert 'P78' in warns
assert 'SR4996' in warns
def test_a_parent_named_by_family_resolves_to_the_number_the_ticket_prints():
"""TWO VOCABULARIES, BOTH LEGITIMATE, MEASURED ON THE LIVE LAYERS.
The plant file names a parent by its FAMILY (`<vendor>_EMBED_P76`); the
job file names it by its PART NUMBER (`SP29`). The ticket prints the part
number. Demanding one spelling would have turned every plant-declared
parent into a finding nobody could act on -- a false finding invented by
the fix.
"""
# The bar itself is NOT printed -- that is the whole case. Only the
# parent is, and only under its family spelling in the knowledge.
printed = [{'text': 'P76 (S.I.F.)', 'view': 'Detail 1'}]
bars = [{'mark': 'SR5602', 'family': 'REBAR', 'length_ft': 1.0}]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, bars, attached={'SR5602': ['VENDOR_EMBED_P76']},
classify=WP115_GRAMMAR)
assert [sev for (sev, _m) in findings] == ['OK']
assert 'SR5602 is attached to VENDOR_EMBED_P76' in findings[0][1]
def test_part_numbers_in_finds_what_mark_tokens_cannot():
"""Exactly one `\\b` apart, and the underscore is why.
In regex `_` is a WORD character, so `\\bP76\\b` never matches
`VENDOR_EMBED_P76` -- the same blindness that let the scrub gate miss
`PREFIX_TERM`. `mark_tokens` keeps its boundary because it reads printed
call-outs, where it is what stops `SR1` being read out of `SR1101`.
"""
assert ticketpage.part_numbers_in('VENDOR_EMBED_P76') == ['P76']
assert ticketpage.mark_tokens('VENDOR_EMBED_P76') == []
assert ticketpage.mark_tokens('SR1101 (E.L.)') == ['SR1101']
def test_a_parent_nothing_can_be_read_out_of_claims_nothing_either_way():
""""The parent is missing" is not something this may assert on the
strength of a lookup it could not perform."""
printed = [{'text': 'SR310 (E.L.)', 'view': 'Detail 1'}]
bars = [{'mark': 'SR310', 'family': 'REBAR', 'length_ft': 8.0},
{'mark': 'SR4996', 'family': 'REBAR', 'length_ft': 1.5}]
findings, _unread = ticketpage.bar_mark_uniqueness_status(
printed, bars, attached={'SR4996': ['THE ANGLE ASSEMBLY']},
classify=WP115_GRAMMAR)
warns = [m for (sev, m) in findings if sev == 'WARN']
assert len(warns) == 1
assert 'could not be established' in warns[0]
assert 'PARENT callout' not in warns[0]
assert 'are never printed on this ticket' not in warns[0]
# ================================ 5.11 - 5.15: solid zones and recesses
#
# Owner, 2026-08-31, having asked before: "没有 solid zone 相关的检查。lifter
# 自带 solid zone,没检查这些 solid zone 是不是被加入了 assembly,是不是被显示
# 在 ticket 上面,hatch pattern 对不对。这些都没有啊" -- then "不光是 solid
# zone,insulation recess 也得加上。"
#
# EVERY PLANT STRING BELOW IS INVENTED except the Revit pattern names, which
# are Autodesk's and identify nobody. A test that pinned this plant's
# `KP ...` type names would put the plant's vocabulary into the repository
# from the test side, which is the same defect as writing it into the check.
# What is asserted is the SHAPE of the answer.
VIEW = 'Layout View 1'
def _zone(member):
return {'id': 1, 'lifter_id': 2, 'member': member}
def test_511_all_members_is_a_genuine_ok():
sev, msg = ticketpage.solid_zone_membership_status(
[_zone(True), _zone(True)])
assert sev == 'OK'
assert '2 solid zone(s)' in msg
def test_511_a_zone_outside_the_assembly_is_a_fail_and_says_why_it_matters():
"""Not a drafting untidiness: the shop is shown the wrong piece."""
sev, msg = ticketpage.solid_zone_membership_status(
[_zone(True), _zone(False), _zone(False)])
assert sev == 'FAIL'
assert '2 of 3' in msg
assert 'assembly-scoped' in msg
def test_511_unreadable_membership_is_never_a_pass_and_never_a_failure():
"""The third answer. It rides the SAME finding, because a second one on
the same code would silently replace the first (6.7's lesson)."""
sev, msg = ticketpage.solid_zone_membership_status(
[_zone(None), _zone(None)])
assert sev is None
assert 'not judged' in msg or 'nothing was judged' in msg
def test_511_a_judged_row_still_names_what_it_could_not_read():
sev, msg = ticketpage.solid_zone_membership_status(
[_zone(True), _zone(None)])
assert sev == 'OK'
assert 'could not be read' in msg
def test_511_no_zone_at_all_is_a_silence_and_not_an_ok():
""""Every zone is a member" is a different statement from "there are no
zones", and this must never print the first when it means the second."""
sev, msg = ticketpage.solid_zone_membership_status([])
assert sev is None
assert 'never printed as one' in msg
def test_511_an_unread_lifter_is_carried_into_the_sentence():
sev, msg = ticketpage.solid_zone_membership_status(
[_zone(True)], lifters_unread=3)
assert sev == 'OK'
assert '3 lifter(s) could not be read' in msg
def test_511_a_lifter_with_no_zone_is_its_own_finding_not_a_membership_one():
"""Zero today (0 of 192, measured 2026-08-31) and built anyway: the day
it fires there is no zone to add, so the repair is in the model."""
assert ticketpage.solid_zone_absent_status(0, 12) is None
sev, msg = ticketpage.solid_zone_absent_status(2, 12)
assert sev == 'FAIL'
assert '2 of 12' in msg
assert 'the model is what has to change' in msg
def test_512_a_non_member_is_reported_as_absent_from_the_ticket():
sev, msg = ticketpage.solid_zone_ticket_status(
[_zone(True), _zone(False)], VIEW)
assert sev == 'FAIL'
assert VIEW in msg
assert 'INFERRED' in msg
def test_512_says_the_visibility_is_inferred_even_when_it_passes():
"""The basis is what tells a reader how to repair it, so it is in the
OK too -- not only in the failure."""
sev, msg = ticketpage.solid_zone_ticket_status([_zone(True)], VIEW)
assert sev == 'OK'
assert 'INFERRED' in msg
def test_512_with_no_view_named_stands_down_naming_that():
sev, msg = ticketpage.solid_zone_ticket_status([_zone(True)], None)
assert sev is None
assert 'not named for this plant' in msg
def test_512_can_be_told_to_stop_inferring():
"""The escape hatch for the day somebody measures the rendered view."""
sev, msg = ticketpage.solid_zone_ticket_status(
[_zone(False)], VIEW, members_are_visible=False)
assert sev is None
assert 'nothing is claimed' in msg
def test_513_a_wrong_hatch_names_both_patterns_and_both_provenances():
""""The hatch is wrong" is not actionable. This is."""
sev, msg = ticketpage.hatch_status(
'the solid zones', 'TRIANG', 'ANSI35', 'legend',
'the legend printed on this ticket draws solid_zone as ANSI35')
assert sev == 'FAIL'
assert 'TRIANG' in msg and 'ANSI35' in msg
assert 'this ticket\'s own legend' in msg
def test_513_a_plant_sourced_expectation_is_named_as_the_weaker_claim():
sev, msg = ticketpage.hatch_status(
'the solid zones', 'TRIANG', 'ANSI35', 'plant', 'because I say so')
assert sev == 'FAIL'
assert 'the plant layer\'s copy of the legend' in msg
def test_513_matching_is_case_insensitive_and_passes():
sev, msg = ticketpage.hatch_status(
'the solid zones', 'ansi35', 'ANSI35', 'legend', 'why')
assert sev == 'OK'
def test_513_no_expectation_at_all_is_a_silence_carrying_its_own_reason():
sev, msg = ticketpage.hatch_status(
'the solid zones', 'TRIANG', None, None, 'nobody wrote it down')
assert sev is None
assert msg == 'nobody wrote it down'
def test_513_an_ungathered_render_is_a_silence_and_never_a_pass():
"""The gather half. It must not read as agreement."""
sev, msg = ticketpage.hatch_status(
'the solid zones', None, 'ANSI35', 'legend', 'the legend says ANSI35')
assert sev is None
assert 'could not be read on this run' in msg
def test_514_stands_down_today_and_names_both_unblockers():
"""It compares nothing, and says so rather than printing a count as a
verdict. That is still worth the row."""
sev, msg = ticketpage.recess_presence_status(
{'2in RECESS': 3, '3in RECESS': 1}, None)
assert sev is None
assert 'NOTHING WAS COMPARED' in msg
assert '2in RECESS x3' in msg
assert 'design-summary reader' in msg
assert 'modelled' in msg
def test_514_carries_the_dormant_modelled_path_note_when_given_one():
sev, msg = ticketpage.recess_presence_status(
{}, None, 'The modelled-recess path was NOT consulted: no ruling.')
assert sev is None
assert 'NOT consulted' in msg
def test_514_becomes_a_real_comparison_the_day_a_requirement_arrives():
"""No new plumbing: the row is written for the requirement it has not
got yet, which is what makes standing down worth doing."""
sev, msg = ticketpage.recess_presence_status(
{'2in RECESS': 2}, {'2in RECESS': 2})
assert sev == 'OK'
sev, msg = ticketpage.recess_presence_status(
{'2in RECESS': 2}, {'2in RECESS': 2, '3in RECESS': 1})
assert sev == 'FAIL'
assert '3in RECESS' in msg
def test_515_the_size_question_is_named_and_never_answered():
caveat = ticketpage.recess_size_caveat()
assert 'not this row\'s question' in caveat
assert 'compares PATTERNS' in caveat
assert 'he has been asked' in caveat
# ---------------------------------------------------------------- the master
# checklist, read the way the BUTTON reads it
#
# THE DEFECT THIS SECTION LOCKS DOWN (2026-08-31, seen live by the owner).
# `ticketpage._read_yaml` prefers pyRevit's YAML reader, which is a YamlDotNet
# wrapper whose whole scalar conversion is `return ynode.Value`. Every scalar
# comes back a STRING, so `null` arrives as the four characters 'null'.
# `knowledge._merge_file` has always coerced after that read; `_read_yaml` did
# not. On the button that made `owner_codes: null` into `('null',)` and
# `checks: null` into `'null'` -- both truthy -- so `CHECKLIST_FOR_ENGINEER`
# derived EMPTY and 5.3/5.5 printed red NOT RUN; and it put the WORD 'null'
# into 37 of 39 rows' `not_run_reason`, where, being a non-empty string, it
# BEAT the derived probe sentence and SATISFIED the guard that demands a
# sentence from every row.
#
# Every test in this file, and every other guard in the suite, was blind to it
# for one reason: host pytest reads through PyYAML, which types its scalars.
# These tests read through a reader that does not. See
# `pyrevit_yaml_fake.py`, which is where that reader's semantics live.
def _import_ticketpage_through(monkeypatch, load_as_dict, name):
"""A FRESH ticketpage, imported with `load_as_dict` as the pyRevit reader.
A fresh module object rather than a reload: `CHECKLIST_ITEMS` is built at
import and much of the package holds references to the live module's
copy, so reloading it in place would leave the rest of the suite reading
a checklist assembled under a test's fake reader.
"""
import importlib.util
import os
from pyrevit_yaml_fake import install_pyrevit_yaml
install_pyrevit_yaml(monkeypatch, load_as_dict)
# The live module's own file: no second copy of where `vibe` lives, and
# no import of `conftest` by name (there are two in this repo).
spec = importlib.util.spec_from_file_location(
name, os.path.abspath(ticketpage.__file__.replace('.pyc', '.py')))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_the_strings_only_reader_really_is_unforgiving():
"""The double has to BITE, or everything below passes for free.
This is the assertion the old yamlkit-backed fake could never make: read
the real catalogue with the real reader's semantics and the nulls come
back as the word.
"""
from pyrevit_yaml_fake import strings_only_yaml_reader
raw = strings_only_yaml_reader()(ticketpage.master_path())
rows = raw['checklist']['master']
assert [r for r in rows if r.get('not_run_reason') == 'null'], (
'the catalogue must still carry `not_run_reason: null` rows for this '
'regression to mean anything')
assert [r for r in rows if r.get('owner_codes') == 'null']
def test_for_engineer_derives_under_the_reader_the_button_actually_uses(
monkeypatch):
"""5.3 and 5.5 are a person's rows on the button too, not only on the host.
They are the only two master rows with `owner_codes: null`, and they are
exactly the two the corruption ate.
"""
from pyrevit_yaml_fake import strings_only_yaml_reader
strict = _import_ticketpage_through(
monkeypatch, strings_only_yaml_reader(), 'ticketpage_stringsonly_fe')
assert strict.CHECKLIST_FOR_ENGINEER == ('5.3', '5.5')
assert strict._for_engineer_rows() == ('5.3', '5.5')
def test_no_row_carries_the_word_null_as_its_reason_on_the_button(monkeypatch):
"""A reason slot holding 'null' is a silent row wearing a sentence.
Non-empty, so the no-empty-reason guard is satisfied; truthy, so it
overrides the derived probe sentence. 37 of 39 rows had it.
"""
from pyrevit_yaml_fake import strings_only_yaml_reader
strict = _import_ticketpage_through(
monkeypatch, strings_only_yaml_reader(), 'ticketpage_stringsonly_rs')
absent = set(strict._NULL_TEXT)
for (ident, _nm, grade, owners, reason, checks) in strict.CHECKLIST_ITEMS:
assert reason is None or reason.strip().lower() not in absent, (
'row {0} carries a spelled-out null as its reason'.format(ident))
assert grade == '' or grade.strip().lower() not in absent, (
'row {0} carries a spelled-out null as its grade'.format(ident))
for owner in (owners or ()):
assert owner.strip().lower() not in absent, (
'row {0} has a null owner code'.format(ident))
for check in strict._check_names(checks):
assert check.strip().lower() not in absent, (
'row {0} names a check function called null'.format(ident))
# THE THIRD SYMPTOM -- nine rows naming a check function called `null` -- is
# NOT re-asserted here, on purpose rather than by omission.
# `test_every_master_entry_names_a_check_that_exists` in
# `test_ticketchecks_host.py` already pairs every named check against the
# module, and it can only run there: importing `vibe.ticketchecks` pulls in
# `vibe.ticketqc`, which imports the Revit API at module scope, and that
# file owns the deliberately inert stub that makes it importable. What
# closes the gap for the button is `test_the_two_readers_agree_row_for_row`
# below: the two tables are identical field for field, so the pairing guard's
# verdict on the host's table is a verdict on the button's.
def test_every_master_id_survives_the_read_as_a_string(monkeypatch):
"""THE 5.10 PIN, and why the fix is null-only rather than `_coerce`.
`knowledge._coerce` also numifies, and coercion runs after parsing, so
the quotes on `id: '5.10'` are long gone by then: it would become the
float 5.1, collide with the real row 5.1, and `load_master` would refuse
the WHOLE checklist -- the button dead rather than merely wrong. So the
absence test is null-only, and ids never go through it at all.
"""
from pyrevit_yaml_fake import strings_only_yaml_reader
strict = _import_ticketpage_through(
monkeypatch, strings_only_yaml_reader(), 'ticketpage_stringsonly_id')
ids = [item[0] for item in strict.CHECKLIST_ITEMS]
for ident in ids:
assert isinstance(ident, str), '{0!r} is not text'.format(ident)
assert len(ids) == len(set(ids))
assert '5.10' in ids and '5.1' in ids
assert ids.index('5.10') != ids.index('5.1')
def test_the_two_readers_agree_row_for_row(monkeypatch):
"""Host and button must build the SAME table, field for field.
The migration was verified field-for-field through a typing reader while
production ran through one that types nothing; that is the blind spot,
and this closes it for every field of every row at once.
"""
from pyrevit_yaml_fake import strings_only_yaml_reader
strict = _import_ticketpage_through(
monkeypatch, strings_only_yaml_reader(), 'ticketpage_stringsonly_all')
assert strict.CHECKLIST_ITEMS == ticketpage.CHECKLIST_ITEMS
def test_is_absent_is_null_only_and_never_touches_a_real_value():
"""It must not swallow a legitimate string, and must not numify."""
assert ticketpage._is_absent(None)
assert ticketpage._is_absent('null')
assert ticketpage._is_absent('NULL')
assert ticketpage._is_absent('~')
assert ticketpage._is_absent(' none ')
assert ticketpage._is_absent('')
assert not ticketpage._is_absent('5.10')
assert not ticketpage._is_absent('check_solid_zone_members')
assert not ticketpage._is_absent('0')
assert not ticketpage._is_absent('nullify the run')
# ------------------------------------------------------ demo-safe: no CJK
#
# Owner, 2026-09-01: no Chinese anywhere in a report or a UI surface, even
# when quoting him -- he is demoing this to colleagues before the weekend.
# The owner's rulings on this checklist (dunnage, weight, note folding --
# `dunnage_clearance`, `weight_three_way`, `note_folds`) now live in `#`
# comments only; the CJK-survival tests elsewhere in this file are about a
# JOB'S OWN mute reason surviving intact, a separate and still-valid
# guarantee. This fixture is the shape a real ticket-QC page renders: the
# checklist table (HTML for the panel, TEXT for `last-ticketqc.txt`) plus
# the style block and the key.
_DEMO_ROWS = [
('4.5', 'debond call-out', 'B', 'WARN',
'Everything of this kind goes out as WARN.'),
('6.4', 'dunnage clearance', 'B', ticketpage.MUTED_STATE,
'no opening -- muted by the owner'),
('8.1', 'reinforcing', 'A', 'OK', 'ran, found nothing'),
]
def test_the_checklist_table_renders_with_no_chinese_text_or_html():
cjk = re.compile(u'[一-鿿]')
html_out = (ticketpage.checklist_table_html(_DEMO_ROWS)
+ ticketpage.CHECKLIST_STYLE + ticketpage.checklist_key_html())
text_out = ticketpage.checklist_table_text(_DEMO_ROWS)
assert not cjk.search(html_out), html_out
assert not cjk.search(text_out), text_out