WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s

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.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
@@ -0,0 +1,676 @@
# -*- coding: utf-8 -*-
"""Tests for `vibe.pa_structural.page()` -- the Structural Model section.
Runs on plain host CPython via `conftest.py`'s `vibe` stub; no Revit
involved and none was running for this test run.
2026-09-04 REWRITE, matching the owner's headless-render ruling the same
day (see `pa_structural.py`'s module docstring for the ruling text):
restraints, concentrated loads and wind zones are now EDITABLE TABLES, not
free-text fields; every ASCE wind input is gone, replaced by a `wind_mode`
choice and a manually-typed `wind_zone` table; Seismic / Temperature /
Soil are collapsed `section` blocks; and the old four-panel
`source: 'elevation'` drawing is gone, replaced by the `pa_sketch_freebody`
free-body block.
Checks the disciplines both this rewrite and the standing docstring commit
to:
* only block kinds from the fixed vocabulary are ever emitted;
* every field carries a plain-English label and a hint;
* every optional free-text block still states a DIFFERENT sentence for
"key missing" vs. "key present but blank" ("could not judge" vs.
"judged, found nothing" must never collapse);
* no wind/seismic/temperature field ever carries a computed default;
* a key `panelanalysisform` does not (yet) define degrades to a
`problems` entry instead of raising or inventing a value;
* `values` is never mutated;
* every ASCE-only wind key is gone from this page;
* `restraint` / `conc_load` / `wind_zone` each render as an
`editable_table` with the exact contract column shape;
* Seismic / Temperature Loads / Soil Loads are collapsed sections;
* no inline (outside a collapsed section) non-quiet sentence runs longer
than one sentence.
"""
import re
from vibe import pa_structural
_BLOCK_KINDS = set(['heading', 'columns', 'field', 'table', 'editable_table',
'section', 'kv', 'sentence', 'drawing', 'rule', 'space'])
_EDITOR_KINDS = set(['text', 'num', 'check', 'choice'])
_ASCE_GONE_KEYS = ('wind_speed_vult', 'wind_risk_category', 'wind_exposure',
'wind_enclosure', 'wind_asce_version', 'wind_cc_chapter',
'wind_mwfrs_chapter', 'wind_pressure_psf',
'wind_suction_psf')
def _walk(blocks):
"""Yield every block, recursing into 'columns' and 'section'."""
for b in blocks:
yield b
if b.get('kind') == 'columns':
for col in b['columns']:
for sub in _walk(col):
yield sub
elif b.get('kind') == 'section':
for sub in _walk(b['blocks']):
yield sub
def _all_field_keys(blocks):
return [b['key'] for b in _walk(blocks) if b.get('kind') == 'field']
def _sentences(blocks):
return [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
def _editable_tables(blocks):
return dict((b['table'], b) for b in _walk(blocks)
if b.get('kind') == 'editable_table')
def _sections(blocks):
return dict((b['title'], b) for b in _walk(blocks)
if b.get('kind') == 'section')
# --------------------------------------------------------------- structure
def test_only_known_block_kinds():
blocks, _problems = pa_structural.page({})
for b in _walk(blocks):
assert b['kind'] in _BLOCK_KINDS, 'unknown block kind {0!r}'.format(
b.get('kind'))
def test_top_level_heading_is_structural_model():
blocks, _problems = pa_structural.page({})
assert blocks[0] == {'kind': 'heading', 'text': 'Structural Model',
'level': 1}
def test_never_raises_on_empty_values():
blocks, problems = pa_structural.page({})
assert isinstance(blocks, list) and blocks
assert isinstance(problems, list)
def test_never_raises_on_none_values():
blocks, problems = pa_structural.page(None)
assert isinstance(blocks, list) and blocks
def test_never_raises_on_garbage_values():
# Wrong types, weird keys, malformed row-table keys -- page() must
# degrade, not crash.
blocks, problems = pa_structural.page({
'conc_loads': None,
'wind_speed_vult': object(),
'supports_ft': '',
'restraint.1.present': 12345,
'restraint.x.present': 'yes',
'restraint..present': 'yes',
'wind_zone.2.present': object(),
3: 'not even a string key',
})
assert isinstance(blocks, list) and blocks
def test_values_dict_not_mutated():
values = {'releases': '', 'restraint.1.present': 'yes',
'restraint.1.elevation_ft': '0'}
before = dict(values)
pa_structural.page(values)
assert values == before
# ------------------------------------------------------------------ fields
def test_every_field_has_plain_english_label_and_hint():
blocks, _problems = pa_structural.page({})
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
assert fields, 'expected at least one field block'
for f in fields:
assert f['label'], 'field {0!r} has no label'.format(f['key'])
# A label that is just the raw key (no spaces, all lower/snake) is
# not plain English -- every field must carry a written-out label.
assert f['label'] != f['key']
assert f['hint'], 'field {0!r} has no hint'.format(f['key'])
assert f['editor'] in _EDITOR_KINDS
assert isinstance(f['required'], bool)
assert 'choices' in f
if f['choices'] is not None:
assert f['editor'] == 'choice'
def test_symbol_is_beside_label_never_instead_of_it():
blocks, _problems = pa_structural.page({})
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
by_key = dict((f['key'], f) for f in fields)
# temp_alpha_t carries a real engineering symbol in panelanalysisform's
# fallback -- the label must still be plain English, not the symbol
# standing in for it.
f = by_key['temp_alpha_t']
assert f['label'].lower() != (f['symbol'] or '').lower()
assert 'thermal' in f['label'].lower() or \
'expansion' in f['label'].lower()
def test_required_bearing_table_present():
# Owner ruling, 2026-09-05: `supports_ft` is no longer rendered as its
# own `field` -- bearing positions are the `support` editable row
# table now (a fallback for an old saved values file, never shown).
blocks, _problems = pa_structural.page({})
tables = [b for b in _walk(blocks) if b.get('kind') == 'editable_table']
by_table = dict((t['table'], t) for t in tables)
assert 'support' in by_table
assert by_table['support']['columns'][0]['col'] == 'x_ft'
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
assert 'supports_ft' not in [f['key'] for f in fields]
def test_old_restraint_dropdown_fields_are_gone():
"""Owner, 2026-09-04: restraints must be an editable table, not the
foundation-connection dropdown + floor-tie checkbox this page used to
render instead of one."""
blocks, _problems = pa_structural.page({})
keys = _all_field_keys(blocks)
assert 'restraint_foundation_connection' not in keys
assert 'restraint_floor_tie' not in keys
# --------------------------------------------------------------- wind gate
def test_no_asce_wind_fields_remain():
"""Owner, 2026-09-04: '前面那些应该在 project-config 里面,从这里移除' --
every ASCE-only wind input is gone from this page."""
blocks, _problems = pa_structural.page({})
keys = set(_all_field_keys(blocks))
for key in _ASCE_GONE_KEYS:
assert key not in keys, '{0} should have been removed'.format(key)
def test_wind_mode_field_is_a_manual_vs_project_config_choice():
blocks, _problems = pa_structural.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['wind_mode']
assert f['editor'] == 'choice'
assert f['choices'] == ['Manual', 'From project config']
def test_wind_project_config_not_built_sentence_is_quiet():
blocks, _problems = pa_structural.page({})
sentences = [b for b in _walk(blocks) if b.get('kind') == 'sentence']
hits = [b for b in sentences if 'not built yet' in b['text'].lower()
and 'project config' in b['text'].lower()]
assert hits, 'expected a quiet sentence about project-config not being built'
for b in hits:
assert b.get('quiet') is True
def test_no_wind_or_seismic_or_temp_field_carries_a_default_value():
"""The owner's ruling, 2026-09-02 (and again 2026-09-04 for wind): "
nothing computes wind/seismic yet, so every such field is blank --
never a pre-filled pressure or guess -- regardless of what else is in
`values`.
"""
blocks, _problems = pa_structural.page({
'length_ft': '40', 'width_ft': '8', 'thickness_in': '6',
})
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
for f in fields:
if f['key'].startswith('seismic_') or f['key'].startswith('temp_'):
assert 'not yet used' in (f['hint'] or '').lower()
# --------------------------------------------------------- editable tables
def test_restraint_table_shape():
blocks, _problems = pa_structural.page({})
tables = _editable_tables(blocks)
t = tables['restraint']
assert t['add_label'] == 'Add restraint'
cols = dict((c['col'], c) for c in t['columns'])
assert cols['elevation_ft']['editor'] == 'num'
assert cols['type']['editor'] == 'choice'
assert cols['type']['choices'] == ['Pin', 'Fixed', 'Free', 'Floor tie']
assert cols['gravity']['editor'] == 'check'
assert cols['thermal']['editor'] == 'check'
assert cols['stiffness_kin']['editor'] == 'num'
assert list(cols.keys()) == ['elevation_ft', 'type', 'gravity',
'thermal', 'stiffness_kin']
def test_conc_load_table_shape():
blocks, _problems = pa_structural.page({})
tables = _editable_tables(blocks)
t = tables['conc_load']
assert t['add_label'] == 'Add load'
cols = dict((c['col'], c) for c in t['columns'])
assert cols['label']['editor'] == 'text'
assert cols['elevation_ft']['editor'] == 'num'
assert cols['offset_in']['editor'] == 'num'
assert cols['dead_k']['editor'] == 'num'
assert cols['live_k']['editor'] == 'num'
assert list(cols.keys()) == ['label', 'elevation_ft', 'offset_in',
'dead_k', 'live_k']
def test_wind_zone_table_shape():
blocks, _problems = pa_structural.page({})
tables = _editable_tables(blocks)
t = tables['wind_zone']
assert t['add_label'] == 'Add wind zone'
cols = dict((c['col'], c) for c in t['columns'])
assert cols['label']['editor'] == 'text'
assert cols['from_elev_ft']['editor'] == 'num'
assert cols['to_elev_ft']['editor'] == 'num'
assert cols['width_ft']['editor'] == 'num'
assert cols['pressure_psf']['editor'] == 'num'
assert cols['suction_psf']['editor'] == 'num'
assert list(cols.keys()) == ['label', 'from_elev_ft', 'to_elev_ft',
'width_ft', 'pressure_psf', 'suction_psf']
def test_editable_tables_are_declarations_not_snapshots():
"""The table blocks never carry row data of their own -- they name the
table and its columns; the renderer reads live rows straight out of
`values` via `row_ids`/`row_key` so an editable cell is a real binding,
not a copy the owner correctly rejected as 'locked'."""
blocks, _problems = pa_structural.page({
'restraint.1.present': 'yes', 'restraint.1.elevation_ft': '0',
'restraint.1.type': 'Pin'})
tables = _editable_tables(blocks)
assert 'rows' not in tables['restraint']
# ------------------------------------------------------- free-body drawing
def test_freebody_drawing_block_matches_contract_exactly():
"""`values={}` has no conc_load/wind_zone rows, so `_freebody_aspect`
-- see its own tests below -- lands on the 0.16 baseline, not the old
flat 0.22."""
blocks, _problems = pa_structural.page({})
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
fb = [d for d in drawings if d.get('module') == 'pa_sketch_freebody']
assert len(fb) == 1
d = fb[0]
assert d['source'] == 'freebody'
assert d['full_width'] is True
assert d['aspect'] == 0.16
assert d['caption'] == 'Free body: restraints and loads'
# 'support' added 2026-09-05 (the bearing-position row table); 'supports_ft'
# kept for a saved panel with no table rows.
assert set(d['depends_on']) == set(['restraint', 'support', 'conc_load',
'wind_zone', 'bottom_elevation_ft',
'top_elevation_ft', 'supports_ft'])
# ------------------------------------------------------- free-body aspect
def _row_values(table, n):
return {'{0}.{1}.present'.format(table, n): 'yes',
'{0}.{1}.elevation_ft'.format(table, n): str(n)}
def test_freebody_aspect_is_016_with_no_load_or_wind_rows():
assert pa_structural._freebody_aspect({}) == 0.16
def test_freebody_aspect_grows_003_per_load_or_wind_row():
values = {}
values.update(_row_values('conc_load', 1))
assert abs(pa_structural._freebody_aspect(values) - 0.19) < 1e-9
values.update(_row_values('wind_zone', 1))
assert abs(pa_structural._freebody_aspect(values) - 0.22) < 1e-9
def test_freebody_aspect_caps_at_028():
values = {}
for n in range(1, 4):
values.update(_row_values('conc_load', n))
for n in range(1, 4):
values.update(_row_values('wind_zone', n))
# 6 rows * 0.03 + 0.16 = 0.34, clamped down to 0.28
assert pa_structural._freebody_aspect(values) == 0.28
def test_freebody_drawing_aspect_matches_helper_with_rows_present():
values = {}
values.update(_row_values('conc_load', 1))
blocks, _problems = pa_structural.page(values)
fb = [b for b in _walk(blocks) if b.get('kind') == 'drawing'
and b.get('module') == 'pa_sketch_freebody'][0]
assert fb['aspect'] == pa_structural._freebody_aspect(values)
def test_old_four_panel_elevation_drawing_is_gone():
"""Owner, 2026-09-04: the free body replaces the old drawing that
crammed front/side/section-properties/cross-section onto one canvas
here."""
blocks, _problems = pa_structural.page({})
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
sources = [d.get('source') for d in drawings]
assert 'elevation' not in sources
captions = [d.get('caption') for d in drawings]
assert not any('restraint and support elevations' in (c or '').lower()
for c in captions)
def test_old_weight_diagram_drawing_is_gone():
"""Same-day follow-up ruling: the free body above already draws every
point load and the member itself, so the old `weight_diagram` drawing
block under Additional Self Weight -- a second, tiny picture repeating
a subset of the same data -- is removed entirely, not merely hidden."""
blocks, _problems = pa_structural.page({})
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
sources = [d.get('source') for d in drawings]
assert 'weight_diagram' not in sources
# exactly one drawing block on the whole page: the free body.
assert len(drawings) == 1
# --------------------------------------------------------- collapsed sections
def test_seismic_temperature_soil_are_collapsed_sections():
"""Owner, 2026-09-04: '"seismic, temp, soil 这几个板块先折叠掉'."""
blocks, _problems = pa_structural.page({})
sections = _sections(blocks)
assert set(sections.keys()) == set([
'Seismic', 'Temperature Loads', 'Soil Loads',
'Additional Self Weight', 'Design Strips'])
for title, sec in sections.items():
assert sec['collapsed'] is True, '{0} should be collapsed'.format(title)
assert isinstance(sec['blocks'], list) and sec['blocks']
def test_additional_self_weight_and_design_strips_are_collapsed_sections():
"""Same-day follow-up: neither block is built yet (nothing in the
solve reads either), so both fold the same way Seismic/Temperature/
Soil already do -- not left open and prominent in the inline flow."""
blocks, _problems = pa_structural.page({'addl_sw_magnitude_plf': ''})
top_level_titles = set(
b.get('text') for b in blocks
if b.get('kind') == 'heading' and b.get('level') == 2)
assert 'Additional Self Weight' not in top_level_titles
assert 'Design Strips' not in top_level_titles
sections = _sections(blocks)
# `design_strips_method` renders unconditionally; the additional-self-
# weight fields only once the key is present (see
# `test_additional_self_weight_fields_appear_once_a_magnitude_is_typed`
# for the 'missing' vs. 'blank' vs. 'filled' distinction) -- blank here
# so both sections' fields are checkable in one call.
asw_keys = _all_field_keys(sections['Additional Self Weight']['blocks'])
ds_keys = _all_field_keys(sections['Design Strips']['blocks'])
assert 'addl_sw_start_in' in asw_keys
assert 'design_strips_method' in ds_keys
def test_additional_self_weight_fields_appear_once_a_magnitude_is_typed():
blocks, _problems = pa_structural.page(
{'addl_sw_magnitude_plf': '25'})
sections = _sections(blocks)
asw_keys = _all_field_keys(sections['Additional Self Weight']['blocks'])
assert 'addl_sw_start_in' in asw_keys
assert 'addl_sw_end_in' in asw_keys
def test_soil_section_keeps_the_exterior_interior_columns_layout():
blocks, _problems = pa_structural.page({})
sections = _sections(blocks)
soil = sections['Soil Loads']
columns_blocks = [b for b in soil['blocks'] if b.get('kind') == 'columns']
assert columns_blocks
assert len(columns_blocks[0]['columns']) == 2
def test_seismic_and_temperature_fields_live_inside_their_sections():
blocks, _problems = pa_structural.page({})
top_level_keys = _all_field_keys(
[b for b in blocks if b.get('kind') != 'section'])
assert 'seismic_cs' not in top_level_keys
assert 'temp_ext_f' not in top_level_keys
sections = _sections(blocks)
seismic_keys = _all_field_keys(sections['Seismic']['blocks'])
temp_keys = _all_field_keys(sections['Temperature Loads']['blocks'])
assert 'seismic_cs' in seismic_keys
assert 'temp_ext_f' in temp_keys
# --------------------------------------------------------------- quiet prose
def test_no_long_inline_non_quiet_helper_prose():
"""Grey helper prose must be quiet (routed to the collapsed Notes
panel); a functional one-sentence status line is fine either way. Only
the INLINE flow is checked here -- content already folded inside a
collapsed `section` is not inline by definition."""
blocks, _problems = pa_structural.page({})
inline = [b for b in blocks if b.get('kind') != 'section']
for b in _walk(inline):
if b.get('kind') != 'sentence':
continue
if b.get('quiet'):
continue
text = b['text']
# crude one-sentence check: at most one terminal period, and it is
# the last character (never "sentence one. sentence two.")
assert text.count('.') <= 1, \
'non-quiet inline sentence looks like more than one ' \
'sentence: {0!r}'.format(text)
def test_restraints_overview_sentence_is_quiet():
blocks, _problems = pa_structural.page({})
sentences = [b for b in _walk(blocks) if b.get('kind') == 'sentence']
hits = [b for b in sentences if 'tied back' in b['text'].lower()]
assert hits
assert hits[0].get('quiet') is True
# ------------------------------------------------------- empty-case sentences
def test_releases_missing_key_says_unknown_not_none():
blocks, problems = pa_structural.page({}) # no 'releases' key at all
sentences = _sentences(blocks)
assert any('unknown' in s.lower() and 'release' in s.lower()
for s in sentences)
assert not any(s == 'Design contains no releases.' for s in sentences)
assert any('releases' in p for p in problems)
def test_releases_blank_key_says_design_contains_no_releases():
blocks, problems = pa_structural.page({'releases': ' '})
sentences = _sentences(blocks)
assert 'Design contains no releases.' in sentences
def test_releases_filled_key_renders_a_table_not_a_sentence():
blocks, _problems = pa_structural.page({'releases': 'Tie 2, sideways'})
tables = [b for b in _walk(blocks) if b.get('kind') == 'table']
assert any('Release' in t['headers'] for t in tables)
def test_exterior_soil_missing_vs_blank_are_different_sentences():
blocks_missing, _ = pa_structural.page({})
blocks_blank, _ = pa_structural.page({'ext_soil_loads': ''})
sm = _sentences(blocks_missing)
sb = _sentences(blocks_blank)
assert 'Design contains no exterior soil loads.' in sb
assert 'Design contains no exterior soil loads.' not in sm
assert any('exterior soil loads' in s.lower() and 'unknown' in s.lower()
for s in sm)
def test_interior_soil_missing_vs_blank_are_different_sentences():
blocks_missing, _ = pa_structural.page({})
blocks_blank, _ = pa_structural.page({'int_soil_loads': ''})
sm = _sentences(blocks_missing)
sb = _sentences(blocks_blank)
assert 'Design contains no interior soil loads.' in sb
assert 'Design contains no interior soil loads.' not in sm
def test_design_strips_blank_matches_eriksson_wording():
blocks, _problems = pa_structural.page({'design_strips_rows': ''})
sentences = _sentences(blocks)
assert ('No design strips are defined; the entire cross section will '
'be used.') in sentences
def test_additional_self_weight_blank_vs_missing():
blocks_missing, _ = pa_structural.page({})
blocks_blank, _ = pa_structural.page({'addl_sw_magnitude_plf': ''})
sm = _sentences(blocks_missing)
sb = _sentences(blocks_blank)
assert any('additional self weight' in s.lower() and 'unknown' in s.lower()
for s in sm)
assert any("panel's own concrete weight" in s.lower() for s in sb)
assert sm != sb
def test_seismic_apply_to_is_a_choice_field():
blocks, _problems = pa_structural.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['seismic_apply_to']
assert f['editor'] == 'choice'
assert f['choices'] == ['None', 'Out-of-plane', 'In-plane', 'Both']
def test_load_opt_offset_from_choices():
blocks, _problems = pa_structural.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['load_opt_offset_from']
assert f['editor'] == 'choice'
assert f['choices'] == ['Deflection Section', 'Top in Form',
'Bottom in Form']
def test_load_opt_pattern_live_is_a_checkbox():
blocks, _problems = pa_structural.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
assert fields['load_opt_pattern_live']['editor'] == 'check'
# ---------------------------------------------------- missing-key degrading
def test_missing_panelanalysisform_keys_become_problems_not_exceptions():
blocks, problems = pa_structural.page({})
assert problems, 'expected at least one problems sentence for the new keys'
for p in problems:
assert isinstance(p, str) or isinstance(p, type(u''))
assert 'Traceback' not in p
def test_new_keys_still_render_with_a_usable_label():
blocks, problems = pa_structural.page({})
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
by_key = dict((f['key'], f) for f in fields)
# 'temp_ext_f' has no entry in panelanalysisform today -- must still
# render with a real label and hint rather than being skipped.
f = by_key['temp_ext_f']
assert f['label'] == 'Exterior temperature'
assert f['hint']
# ------------------------------------------------------------- row_ids/row_key
def test_row_key_shape():
assert pa_structural.row_key('restraint', '1', 'elevation_ft') == \
'restraint.1.elevation_ft'
assert pa_structural.row_key('conc_load', 'u3', 'dead_k') == \
'conc_load.u3.dead_k'
def test_row_ids_reads_present_yes_and_sorts_numerically():
values = {
'restraint.2.present': 'yes', 'restraint.2.elevation_ft': '10',
'restraint.10.present': 'yes', 'restraint.10.elevation_ft': '30',
'restraint.1.present': 'yes', 'restraint.1.elevation_ft': '0',
'restraint.5.present': 'no', # not 'yes' -- excluded
'restraint.5.elevation_ft': '99',
'conc_load.1.present': 'yes', # a different table -- excluded
}
assert pa_structural.row_ids('restraint', values) == [1, 2, 10]
def test_row_ids_ignores_malformed_keys():
values = {'restraint.x.present': 'yes', 'restraint..present': 'yes',
'restraint.1.2.present': 'yes', 'restraint.3.present': 'yes'}
assert pa_structural.row_ids('restraint', values) == [3]
def test_row_ids_empty_on_empty_values():
assert pa_structural.row_ids('restraint', {}) == []
assert pa_structural.row_ids('restraint', None) == []
# ------------------------------------------------------------------ leaks
def test_no_client_or_job_strings_leak_into_the_page():
"""Nothing this page emits may carry an identity.
The obvious version of this test spells the forbidden strings out --
and a leak detector that names the secret IS the leak, which is exactly
why `.scrub-terms` is gitignored ("so the list of names is not itself
the leak"). So this asks about SHAPE instead: an emitted string that
looks like a job number, a run date, a source-file name or a licensee
line is refused whatever it says.
The named-terms half of this belongs to `tools/scrub.py`, which reads
that private list. This is the half that still works on a machine where
the list is absent.
"""
blocks, problems = pa_structural.page({})
text = repr(blocks) + repr(problems)
assert not re.search(r'\d{5,}', text) # a job number
assert not re.search(r'\d{2}/\d{2}/\d{4}', text) # a run date
assert not re.search(r'\.(ewf|rvt)', text, re.I) # a source file
assert 'licensee' not in text.lower()
# ---------------------------------------------------------------- issue #22
def test_free_body_is_the_last_block_on_the_page():
# Owner, 2026-09-07 (issue #22, item 6b): "把 free body 的图挪到页面的
# 最底下" -- after every collapsed section, so everything it draws is
# entered above it. Top-level order, not the recursive walk: the
# drawing must be the final entry of `page()`'s own list.
blocks, _problems = pa_structural.page({})
last = blocks[-1]
assert last['kind'] == 'drawing' and last['source'] == 'freebody'
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
assert len(drawings) == 1
# and the collapsed sections all sit before it
idx_last_section = max(i for i, b in enumerate(blocks)
if b.get('kind') == 'section')
assert idx_last_section < len(blocks) - 1
def test_support_and_restraint_tables_carry_an_empty_state():
# Issue #22, items 5/11: an empty `support` table must say why it is
# empty -- the read's own `supports_basis` / `restraints_basis`
# sentence when there is one, else a could-not-judge sentence -- never
# a bare "Add support" button. The `support` one also says the solve
# needs at least two rows, so the gap shows before Run, not after.
blocks, _problems = pa_structural.page({})
tables = _editable_tables(blocks)
sup = tables['support']['empty_state']
assert sup['basis_keys'][0] == 'supports'
assert 'at least two' in sup['text']
assert 'could-not-judge' in sup['text']
res = tables['restraint']['empty_state']
assert res['basis_keys'][0] == 'restraints'
# tables with no model read (conc_load, wind_zone) carry none: the
# page's generic "nothing entered yet" line covers them
assert 'empty_state' not in tables['conc_load']