# -*- 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. `
`/`` 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 ('')[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('')[0] assert _CSV not in clip # not inside the clipped div assert 'wt-ck-note-path' in html assert '>' + _CSV + '' 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('
c', 'A', 'OK', 'x & y')]) assert '<b>' in html assert 'x & 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" 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 '' 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 + '' 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 '` 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 + '' 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 (`_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