Files
willtools-demo/WillTools.extension/tests/test_jobrecon.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

710 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Tests for the job-level reconciliation core.
NO REAL MARK, CONTROL NUMBER, DESIGN NUMBER OR JOB DETAIL APPEARS HERE.
Everything is invented: marks PNL1xx / BM1xx, controls in a 9xxxx band that
no real job uses, designs PNL1.0 / RB1. The reconciliation is arithmetic on
strings; it does not need a real job to be exercised, and the repo must never
carry one.
The shapes under test are the ones that have actually bitten, or that would
bite silently if they broke:
* a printed RANGE is several pieces, not one token
* a stripped page 2 is still a ticket page
* 'could not judge' never renders as 'judged and clean'
* nothing is filtered for looking out of scope
"""
from vibe import jobrecon
def piece(control, mark, design='PNL1.0', **kw):
row = {'id': kw.pop('id', hash(control) % 100000), 'category': 'X',
'control': control, 'mark': mark, 'design': design,
'family': kw.pop('family', 'PANEL'), 'in_assembly': None,
'ticket_created': None, 'ticket_detailed': None}
row.update(kw)
return row
def sheet(name, number, controls, design='PNL1.0', **kw):
row = {'id': kw.pop('id', 1), 'number': number, 'name': name,
'design': design, 'controls': list(controls),
'total_required': None, 'sheet_count': None}
row.update(kw)
return row
# ------------------------------------------------- expand_control_field
def test_plain_control_number():
assert jobrecon.expand_control_field('90001') == (['90001'], [])
def test_comma_list_is_several_pieces():
got, bad = jobrecon.expand_control_field('90001, 90003 ,90007')
assert got == ['90001', '90003', '90007']
assert bad == []
def test_range_expands_to_every_piece_in_it():
"""A slot holding a range is four pieces. Reading it as one token
under-counts the ticket by three and invents three orphans."""
got, bad = jobrecon.expand_control_field('90120-90123')
assert got == ['90120', '90121', '90122', '90123']
assert bad == []
def test_range_keeps_leading_zero_width():
got, _bad = jobrecon.expand_control_field('008-011')
assert got == ['008', '009', '010', '011']
def test_en_dash_range_still_expands():
got, bad = jobrecon.expand_control_field(u'90120–90122')
assert got == ['90120', '90121', '90122']
assert bad == []
def test_absurd_range_is_reported_not_materialised():
got, bad = jobrecon.expand_control_field('1-99999')
assert got == []
assert bad == ['1-99999']
def test_backwards_range_is_unreadable_not_empty():
got, bad = jobrecon.expand_control_field('90123-90120')
assert got == []
assert bad == ['90123-90120']
def test_material_text_is_returned_as_unreadable_never_dropped():
"""The same title-block slots carry a material list on non-ticket
sheets. A slot the tool cannot read is a slot whose pieces it cannot
vouch for, so it comes back rather than vanishing."""
got, bad = jobrecon.expand_control_field('PL 1/4" X 28" X 4\'-0"')
assert got == []
assert bad
def test_empty_slot_is_neither_control_nor_complaint():
assert jobrecon.expand_control_field(None) == ([], [])
assert jobrecon.expand_control_field(' ') == ([], [])
# ------------------------------------------------------- classify_sheet
def test_sheet_named_after_a_mark_is_a_ticket():
kind, _why = jobrecon.classify_sheet(
sheet('PNL101', '1 OF 2', ['90001']), {'PNL101'})
assert kind == 'ticket'
def test_stripped_page_two_is_still_a_ticket_page():
"""A multi-page ticket's later sheets carry no title block: no design
number, no length, no weight. An and-gate on both signals drops them
and every finding on those pages disappears."""
kind, why = jobrecon.classify_sheet(
sheet('PNL101', '2 OF 2', [], design=None), {'PNL101'})
assert kind == 'ticket'
assert 'stripped' in why
def test_page_numbered_title_block_is_a_ticket_even_when_unnamed():
kind, _why = jobrecon.classify_sheet(
sheet('SOMETHING ELSE', '1 OF 2', ['90001']), {'PNL101'})
assert kind == 'ticket'
def test_ordinary_drawing_is_not_a_ticket():
kind, _why = jobrecon.classify_sheet(
sheet('FOUNDATION PLAN', 'F1.0', [], design=None), {'PNL101'})
assert kind == 'other'
def test_missing_space_in_page_number_still_reads():
assert jobrecon.page_of(sheet('PNL101', '1OF 1', [])) == (1, 1)
# ------------------------------------------------------ page_set_status
def test_complete_page_set_is_ok():
status, _msg = jobrecon.page_set_status([(1, 2), (2, 2)])
assert status == 'ok'
def test_missing_page_is_broken_and_names_the_page():
status, msg = jobrecon.page_set_status([(1, 3), (3, 3)])
assert status == 'broken'
assert '2' in msg
def test_no_page_markers_is_unjudged_not_ok():
"""The criterion source is absent, so nothing was judged. 'ok' here is
the exact 'ran and said fine while seeing nothing' failure."""
status, msg = jobrecon.page_set_status([(None, None), (None, None)])
assert status == 'unjudged'
assert 'not be judged' in msg
def test_pages_disagreeing_on_the_total_is_broken():
status, _msg = jobrecon.page_set_status([(1, 6), (2, 2)])
assert status == 'broken'
# ------------------------------------------------------------ reconcile
def test_a_piece_with_no_ticket_is_named_individually():
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL102')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['pieces_uncovered'] == 1
assert [x['control'] for x in r['pieces_without_ticket']] == ['90002']
assert r['pieces_without_ticket'][0]['marks'] == ['PNL102']
def test_a_ticket_for_a_piece_that_does_not_exist_is_named():
pieces = [piece('90001', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 1', ['90001, 90099'])]
r = jobrecon.reconcile(pieces, sheets)
assert [x['control'] for x in r['controls_without_piece']] == ['90099']
assert r['controls_without_piece'][0]['sheets'] == ['PNL101']
def test_range_on_a_ticket_covers_every_piece_in_it():
pieces = [piece('90120', 'BM100', design='RB1'),
piece('90121', 'BM100', design='RB1'),
piece('90122', 'BM100', design='RB1')]
sheets = [sheet('BM100', '1 OF 1', ['90120-90122'], design='RB1')]
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['pieces_uncovered'] == 0
assert r['mark_count_mismatch'] == []
def test_count_mismatch_names_both_sides():
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 1', ['90001', '90050'])]
r = jobrecon.reconcile(pieces, sheets)
row = r['mark_count_mismatch'][0]
assert row['mark'] == 'PNL101'
assert row['issued'] == 2 and row['modelled'] == 2
assert row['issued_only'] == ['90050']
assert row['modelled_only'] == ['90002']
def test_mark_with_no_ticket_at_all_is_its_own_finding():
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL102')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
r = jobrecon.reconcile(pieces, sheets)
assert [x['mark'] for x in r['marks_without_ticket']] == ['PNL102']
assert r['marks_without_ticket'][0]['pieces'] == 1
def test_stripped_page_two_does_not_read_as_a_missing_ticket():
"""Regression for the classifier trap: page 2 has no design number and
no control numbers. If it is dropped the set looks incomplete; if it is
counted as a silent page the piece is still covered by page 1."""
pieces = [piece('90001', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 2', ['90001']),
sheet('PNL101', '2 OF 2', [], design=None)]
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['pieces_uncovered'] == 0
assert r['page_set_problems'] == []
assert len(r['silent_ticket_pages']) == 1
def test_quantity_stated_but_not_printed_is_a_finding():
"""A title block with a fixed number of control slots can state a
quantity larger than it can print. The pieces past the last slot are
unaccounted for on that ticket."""
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 1', ['90001', '90002'],
total_required='9')]
r = jobrecon.reconcile(pieces, sheets)
assert r['quantity_disagreements'][0]['stated'] == 9
assert r['quantity_disagreements'][0]['listed'] == 2
def test_ticket_design_number_disagreeing_with_its_pieces():
pieces = [piece('90001', 'PNL101', design='PNL4.0')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'], design='PNL2.0')]
r = jobrecon.reconcile(pieces, sheets)
row = r['design_disagreements'][0]
assert row['printed'] == 'PNL2.0' and row['pieces_say'] == ['PNL4.0']
def test_stated_sheet_count_disagreeing_with_sheets_found():
pieces = [piece('90001', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 2', ['90001'], sheet_count='1'),
sheet('PNL101', '2 OF 2', [], design=None)]
r = jobrecon.reconcile(pieces, sheets)
assert r['stated_page_counts'][0]['stated'] == 1
assert r['stated_page_counts'][0]['found'] == 2
def test_model_claiming_a_ticket_that_does_not_exist():
pieces = [piece('90002', 'PNL102', ticket_created='Yes')]
r = jobrecon.reconcile(pieces, [])
assert r['state_contradictions'][0]['control'] == '90002'
assert 'was created' in r['state_contradictions'][0]['problem']
def test_piece_missing_from_a_ticket_that_exists_is_separated_out():
"""The worst shape here, and the one a per-ticket checker cannot see:
the ticket for the mark exists and passes every check on it, and this
piece's control number is not printed on it."""
pieces = [piece('90001', 'PNL101'),
piece('90002', 'PNL101', ticket_created='Yes')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
r = jobrecon.reconcile(pieces, sheets)
rows = {x['control']: x for x in r['pieces_without_ticket']}
assert rows['90002']['mark_has_ticket'] is True
assert 'does not print this control number' in \
r['state_contradictions'][0]['problem']
text = '\n'.join(jobrecon.format_report(r))
assert 'PIECES MISSING FROM A TICKET THAT EXISTS -- 1 piece(s)' in text
# The other bucket found nothing, and 'found nothing' is still printed --
# by name, in the folded section, never as a heading over the word 'none'.
assert 'PIECES WITH NO TICKET FOR THEIR MARK AT ALL -- ran, found none' \
in text
def test_piece_whose_mark_was_never_drawn_is_the_other_bucket():
pieces = [piece('90002', 'PNL102', ticket_created='Yes')]
r = jobrecon.reconcile(pieces, [])
assert r['pieces_without_ticket'][0]['mark_has_ticket'] is False
assert 'there is no ticket' in r['state_contradictions'][0]['problem']
def test_model_saying_no_ticket_while_a_ticket_prints_the_piece():
pieces = [piece('90001', 'PNL101', ticket_created='No')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['state_contradictions'][0]['control'] == '90001'
def test_unset_ticket_flag_is_not_a_contradiction():
"""None is not False. 'the model never said' and 'the model said no' are
different facts, and only the second is a contradiction."""
pieces = [piece('90002', 'PNL102', ticket_created=None)]
r = jobrecon.reconcile(pieces, [])
assert r['state_contradictions'] == []
def test_one_control_number_on_several_elements_is_reported():
"""Three elements sharing a control number is one piece modelled three
ways. It is counted once as a piece and reported once as a fact -- never
filtered, whatever the families are called."""
pieces = [piece('90200', 'BM100', family='BEAM', id=1),
piece('90200', 'BM100', family='BEAM_FLAT (DO NOT USE)', id=2),
piece('90200', 'BM100', family='BEAM_WARPED (DO NOT USE)', id=3)]
sheets = [sheet('BM100', '1 OF 1', ['90200'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['elements'] == 3
assert r['totals']['pieces'] == 1
assert r['totals']['pieces_uncovered'] == 0
row = r['shared_controls'][0]
assert row['count'] == 3
assert 'BEAM_FLAT (DO NOT USE)' in row['families']
def test_an_element_with_no_control_number_is_a_blind_spot_not_a_pass():
pieces = [piece('90001', 'PNL101'), piece('', 'PNL199', id=4242)]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['pieces'] == 1
assert r['pieces_without_control'][0]['id'] == 4242
assert any('no control number' in b for b in r['blind_spots'])
def test_unreadable_slot_becomes_a_blind_spot():
pieces = [piece('90001', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 1', ['90001', 'SEE SCHEDULE'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['unreadable_control_slots'][0]['token'] == 'SEE SCHEDULE'
assert any('cannot read' in b for b in r['blind_spots'])
def test_a_genuinely_clean_job_says_so_and_has_no_blind_spots():
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 1', ['90001, 90002'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['pieces_uncovered'] == 0
assert r['controls_without_piece'] == []
assert r['mark_count_mismatch'] == []
assert r['blind_spots'] == []
def test_coverage_by_design_shows_a_wholly_unticketed_design():
pieces = [piece('90001', 'PNL101', design='PNL1.0'),
piece('90300', 'BM100', design='RB1'),
piece('90301', 'BM101', design='RB1')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
r = jobrecon.reconcile(pieces, sheets)
assert r['coverage_by_design']['RB1'] == {
'pieces': 2, 'covered': 0, 'uncovered': ['90300', '90301']}
assert r['coverage_by_design']['PNL1.0']['covered'] == 1
def test_nothing_is_excluded_for_looking_out_of_scope():
"""The original sin this module is built against: one unauthorised
exclusion hid forty tickets and every check that ran came back clean.
A piece whose family is literally named 'DO NOT USE' is still a piece."""
pieces = [piece('90400', 'BM199', family='ZZ_SCRATCH (DO NOT USE)')]
r = jobrecon.reconcile(pieces, [])
assert [x['control'] for x in r['pieces_without_ticket']] == ['90400']
# --------------------------------------------------------- format_report
def test_report_names_pieces_rather_than_counting_them():
pieces = [piece('90002', 'PNL102')]
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, [])))
assert '90002' in text and 'PNL102' in text
def test_report_says_what_it_could_not_judge():
pieces = [piece('', 'PNL199', id=7)]
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, [])))
assert 'COULD NOT JUDGE' in text
assert 'no control number' in text
def test_report_truncation_admits_it_truncated():
pieces = [piece(str(90000 + n), 'PNL1' + str(n)) for n in range(40)]
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, []),
limit=5))
assert 'and 35 more (not printed)' in text
def test_report_on_a_clean_job_says_nothing_was_unseen():
pieces = [piece('90001', 'PNL101')]
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, sheets)))
assert 'every piece and every sheet was read' in text
# ------------------------------------------------------- the declared scope
#
# The reconciliation is the one entry point that sweeps the WHOLE job, so it
# is the one where judging another engineer's pieces does the most damage:
# every beam ticket becomes a disagreement, and the panels' real findings
# drown in them. These tests pin the three states -- undeclared, declared and
# filtering, declared and filtering nothing -- and above all that what was
# set aside is COUNTED AND NAMED rather than missing.
from vibe import scope as scopelib
def _mixed():
pieces = [piece('1', 'PNL001'), piece('2', 'PNL002'),
piece('3', 'XX001', design='XX1'),
piece('4', 'YY001', design='YY1')]
sheets = [sheet('PNL001', '1 OF 1', ['1']),
sheet('XX001', '1 OF 1', ['3'], design='XX1'),
sheet('GENERAL ARRANGEMENT', 'S-101', [], design='')]
return pieces, sheets
def test_no_scope_declared_reconciles_every_piece():
"""An undeclared scope is not an empty scope. Nothing is set aside and
the report still carries the line saying so."""
pieces, sheets = _mixed()
r = jobrecon.reconcile(pieces, sheets)
assert r['totals']['pieces'] == 4
assert r['out_of_scope']['declared'] is False
assert r['out_of_scope']['piece_count'] == 0
assert 'No scope is declared' in r['out_of_scope']['piece_line']
def test_a_declared_scope_sets_the_other_engineers_pieces_aside():
pieces, sheets = _mixed()
r = jobrecon.reconcile(pieces, sheets,
scope=scopelib.Scope(prefixes=('PNL',)))
assert r['totals']['pieces'] == 2
assert r['out_of_scope']['piece_count'] == 2
assert r['out_of_scope']['pieces'] == [('XX', 1), ('YY', 1)]
# And nothing about them is reported as a finding -- neither clean nor
# failing. They are simply not in the arithmetic. PNL002 genuinely has
# no ticket and is still reported; the other engineers' pieces are not.
found = [m for row in r['pieces_without_ticket'] for m in row['marks']]
assert found == ['PNL002']
def test_the_out_of_scope_ticket_is_not_reported_as_an_orphan():
"""The failure this prevents: another engineer's ticket printing control
numbers whose pieces were filtered out reads as a fault on his work."""
pieces, sheets = _mixed()
r = jobrecon.reconcile(pieces, sheets,
scope=scopelib.Scope(prefixes=('PNL',)))
assert r['controls_without_piece'] == []
assert r['out_of_scope']['sheet_count'] == 1
def test_a_sheet_that_is_not_a_mark_is_never_treated_as_somebody_elses():
pieces, sheets = _mixed()
r = jobrecon.reconcile(pieces, sheets,
scope=scopelib.Scope(prefixes=('PNL',)))
kinds = dict(r['out_of_scope']['sheets'])
assert 'GENERAL' not in kinds
assert kinds == {'XX': 1}
def test_a_piece_with_no_readable_mark_is_reconciled_not_dropped():
pieces = [piece('1', 'PNL001'), piece('2', '')]
r = jobrecon.reconcile(pieces, [], scope=scopelib.Scope(prefixes=('PNL',)))
assert r['totals']['pieces'] == 2
assert r['out_of_scope']['piece_count'] == 0
assert 'no readable mark prefix' in r['out_of_scope']['piece_line']
def test_the_report_prints_what_it_did_not_judge_as_its_own_section():
pieces, sheets = _mixed()
r = jobrecon.reconcile(pieces, sheets,
scope=scopelib.Scope(prefixes=('PNL',),
declared_by='somebody'))
text = '\n'.join(jobrecon.format_report(r))
assert 'WHAT THIS RUN DID NOT JUDGE -- OUTSIDE THE DECLARED SCOPE' in text
assert 'XX 1' in text and 'YY 1' in text
assert 'scope declared by somebody' in text
# The two states stay separate: not judged, and could not be judged.
assert 'WHAT THIS RUN COULD NOT JUDGE' in text
def test_the_scope_section_is_printed_even_when_nothing_was_set_aside():
pieces = [piece('1', 'PNL001')]
r = jobrecon.reconcile(pieces, [], scope=scopelib.Scope(prefixes=('PNL',)))
text = '\n'.join(jobrecon.format_report(r))
assert 'WHAT THIS RUN DID NOT JUDGE -- OUTSIDE THE DECLARED SCOPE' in text
assert 'none -- nothing was set aside' in text
# --------------------------------------------------------- the folding page
#
# THE RULE, and whose it is: the owner, 2026-08-30, now standing across the
# tab. The answer is the headline and is never folded; everything else is
# collapsed; a section that carries a problem opens itself; a folded header
# still says what it holds. The contract the reader gets is that NOTHING OPEN
# BELOW THE HEADLINE MEANS NOTHING NEEDS HIM -- so the tests that matter most
# here are the two that guard it in both directions: a clean run opens
# nothing, and a run with a finding opens exactly that finding.
#
# The fold itself is `vibe.foldreport`'s and is tested there. These are about
# which sections this report has, what is in the headline, and what may never
# be folded into silence.
from vibe import foldreport as fold
def _clean():
return jobrecon.reconcile([piece('90001', 'PNL101')],
[sheet('PNL101', '1 OF 1', ['90001'])])
def _by_key(sections):
return dict((s.key, s) for s in sections)
def _md(section):
return '\n'.join(b[1] for b in section.blocks if b[0] == fold.MD)
def test_the_headline_is_first_and_is_never_folded():
sections = jobrecon.render(_clean())
assert sections[0].headline is True
assert sections[0].is_open is True
text = _md(sections[0])
assert 'JOB RECONCILIATION' in text
# The verdict is the NUMBERS, not the findings.
assert '1 of 1 pieces are printed on a ticket' in text
assert '1 of 1 marks have a ticket' in text
def test_a_clean_run_opens_nothing_below_the_headline():
"""The whole contract. If this fails the page is lying to the reader."""
sections = jobrecon.render(_clean())
assert [s.key for s in sections[1:] if s.is_open] == []
def test_a_finding_opens_its_own_section_and_says_why():
pieces = [piece('90001', 'PNL101'),
piece('90002', 'PNL101', ticket_created='Yes')]
sections = jobrecon.render(
jobrecon.reconcile(pieces, [sheet('PNL101', '1 OF 1', ['90001'])]))
opened = _by_key(fold.sections_with_problems(sections[1:]))
assert 'missing_from_ticket' in opened
# A section that opens has to name what is wrong, not merely be open.
assert '1 piece(s)' in opened['missing_from_ticket'].problem
def test_the_headline_carries_the_count_of_every_finding():
"""A headline saying 'clean' over an open section below it is the page
this replaces. The counts are built from the same list the sections are,
so the two cannot disagree."""
pieces = [piece('90002', 'PNL102'), piece('90003', 'PNL103')]
recon = jobrecon.reconcile(pieces, [])
sections = jobrecon.render(recon)
tables = [b for b in sections[0].blocks if b[0] == fold.TABLE]
counted = dict((row[0], row[1]) for row in tables[0][2])
opened = [s for s in sections[1:] if s.problem and s.key != 'blind_spots']
assert len(counted) == len(opened)
assert counted['PIECES WITH NO TICKET FOR THEIR MARK AT ALL'] == 2
def test_every_check_is_either_a_section_or_named_as_having_found_nothing():
"""Folded is not gone, and a check that passed is not gone either:
'ran and found none' and 'never ran' have to stay tellable apart."""
recon = _clean()
sections = jobrecon.render(recon)
keys = set(s.key for s in sections)
printed = _md(_by_key(sections)['clear'])
for key, title, _rows, _phrase, _line, _lead in jobrecon.findings(recon):
assert key in keys or title in printed
def test_a_routine_section_is_folded_and_its_header_says_what_it_holds():
coverage = _by_key(jobrecon.render(_clean()))['coverage']
assert coverage.is_open is False
assert '1 design(s)' in coverage.summary
def test_the_out_of_scope_header_carries_its_counts_while_folded():
"""Read with the fold SHUT. A header saying 'details' would soften
exactly the distinction this section exists to keep."""
pieces = [piece('1', 'PNL001'), piece('2', 'BM001', design='BM1'),
piece('3', 'BM002', design='BM1'),
piece('4', 'SS001', design='SS1')]
recon = jobrecon.reconcile(pieces, [],
scope=scopelib.Scope(prefixes=('PNL',)))
section = _by_key(jobrecon.render(recon))['out_of_scope']
assert section.is_open is False
assert 'OUT OF SCOPE: 3 model element(s)' in section.summary
assert 'BM 2' in section.summary and 'SS 1' in section.summary
def test_the_blind_spot_header_carries_its_count_while_folded():
recon = jobrecon.reconcile([piece('90001', 'PNL101'),
piece('', 'PNL199', id=7)],
[sheet('PNL101', '1 OF 1', ['90001'])])
section = _by_key(jobrecon.render(recon))['blind_spots']
assert section.is_open is False
assert '1 blind spot(s)' in section.summary
def test_could_not_see_and_did_not_judge_are_never_the_same_section():
recon = jobrecon.reconcile([piece('1', 'PNL001'),
piece('2', 'BM001', design='BM1')],
[], scope=scopelib.Scope(prefixes=('PNL',)))
keys = [s.key for s in jobrecon.render(recon)]
assert 'out_of_scope' in keys and 'blind_spots' in keys
def test_a_refusal_is_a_problem_and_opens_itself():
"""Nothing reconciled is not a clean job. It opens the blind spots and
it is said again in the headline, because a refusal reported quietly
reads exactly like a pass."""
recon = jobrecon.reconcile([], [])
recon['blind_spots'].insert(
0, 'the role member.control_number is not mapped -- no piece could '
'be identified, so NOTHING was reconciled.')
sections = jobrecon.render(recon)
spots = _by_key(sections)['blind_spots']
assert spots.is_open is True
assert 'NOTHING WAS RECONCILED' in spots.problem
headline = _md(sections[0])
assert 'NOTHING WAS RECONCILED' in headline
assert 'Nothing needs you' not in headline
def test_an_empty_model_is_a_refusal_even_with_no_note_about_it():
"""Structural, not only from the gatherer's note: a run can reconcile
nothing without anybody having written a note saying so."""
assert jobrecon.blind_refusal(jobrecon.reconcile([], [])) is not None
assert jobrecon.blind_refusal(_clean()) is None
def test_sheets_that_no_page_classified_as_a_ticket_is_a_refusal():
recon = jobrecon.reconcile([piece('90001', 'PNL101')],
[sheet('GENERAL ARRANGEMENT', 'S-101', [],
design='')])
assert 'NO SHEET WAS RECOGNISED AS A TICKET' in \
jobrecon.blind_refusal(recon)
def test_a_refusal_does_not_let_the_clean_checks_read_as_a_pass():
recon = jobrecon.reconcile([], [])
clear = _by_key(jobrecon.render(recon))['clear']
assert 'not a pass' in clear.summary
def test_the_page_caps_a_long_list_and_the_file_does_not():
pieces = [piece(str(90000 + n), 'PNL1' + str(n)) for n in range(40)]
recon = jobrecon.reconcile(pieces, [])
page = fold.text(jobrecon.render(recon, limit=5))
assert 'and 35 more (not printed)' in page
# The file is the copy nothing is lost from: no cap, nothing folded.
whole = '\n'.join(jobrecon.format_report(recon))
assert 'not printed' not in whole
assert '90039' in whole
def test_the_file_prints_the_folded_sections_too():
"""A folded section that will not open must cost a file open, never a
sentence -- so `format_report` shows everything by default."""
text = '\n'.join(jobrecon.format_report(_clean()))
assert 'COVERAGE BY DESIGN' in text
assert 'PAGE-SET INVENTORY' in text
assert 'CHECKS THAT RAN AND FOUND NOTHING' in text
def test_what_was_swept_folds_apart_from_what_could_not_be_seen():
"""`jobreconscan` hands over one list of notes: 'I opened these
collectors' is an inventory, 'I could not see X' is a blind spot, and the
report folds the first while the second can open itself."""
recon = _clean()
recon['searched'] = ['categories searched: OST_Walls.']
section = _by_key(jobrecon.render(recon))['searched']
assert section.is_open is False
assert 'OST_Walls' in _md(section)
def test_the_page_names_the_file_the_whole_report_went_to():
sections = _by_key(jobrecon.render(_clean(), full_path='X/Y.txt'))
assert 'X/Y.txt' in _md(sections['run'])
def test_no_header_carries_an_entity_the_panel_will_not_decode():
"""This page writes ASCII '--' in a title, a summary and a problem, so the
one string is right in the panel and in the .md alike. (`foldreport` does
restore the reports' entities in those three since 2026-08-30, so an
`—` would no longer leak -- but this page does not need one.)"""
pieces = [piece('1', 'PNL001'), piece('2', 'BM001', design='BM1')]
recon = jobrecon.reconcile(pieces, [],
scope=scopelib.Scope(prefixes=('PNL',)))
recon['timing'] = {'total_s': 1, 'pieces_s': 1, 'sheets_s': 0}
for section in jobrecon.render(recon):
for value in (section.title, section.summary, section.problem):
assert '&' not in (value or '')
def test_every_section_key_is_unique_on_the_page():
"""They become element ids for the toggle. Two of the same and one fold
opens the other one's body."""
pieces = [piece('1', 'PNL001'), piece('2', 'BM001', design='BM1')]
recon = jobrecon.reconcile(pieces, [],
scope=scopelib.Scope(prefixes=('PNL',)))
keys = [s.key for s in jobrecon.render(recon)]
assert len(keys) == len(set(keys))
def test_the_panel_html_renders_and_a_problem_section_needs_no_script():
"""The toggle is IE5-era DOM and has not been exercised inside Revit, so
a section with a problem in it must render OPEN as plain HTML."""
sections = jobrecon.render(jobrecon.reconcile([piece('90002', 'PNL102')],
[]))
html = fold.html(sections[1:])
# Every newline starts a new entry in pyRevit's panel stream. So: none.
assert '\n' not in html
assert 'display:block' in html
assert '<details' not in html