WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The full view of the configuration: label-first, and what opens itself.
|
||||
|
||||
`vibe.configpage` is the SECONDARY surface of the Project Config button. The
|
||||
button itself is a four-module window of controls (`vibe.configform`); this is
|
||||
the "Full view" behind it -- every setting, every layer, and the provenance of
|
||||
every value -- printed to the output panel and written whole to a file beside
|
||||
the model. It has no Revit in it: the button passes in the layers it read, and
|
||||
everything below is that contract exercised on host CPython.
|
||||
|
||||
Two of these tests are regressions for defects the owner hit on one press on
|
||||
2026-08-30, and they are assertions rather than comments because both read as
|
||||
cosmetic and neither was:
|
||||
|
||||
* `test_no_row_is_named_by_its_dotted_key` -- the screen offered
|
||||
`handling.report_dir` as the name of an option when the row had carried
|
||||
the label "Report folder" all along.
|
||||
* `test_*_opens_itself` -- the fold contract: if nothing below the headline
|
||||
is open, nothing below needs the reader. A section that opens has to say
|
||||
why in a sentence.
|
||||
|
||||
And one is the seam with the window: `test_every_row_says_where_it_is_changed`
|
||||
keeps the two surfaces from becoming two answers to the same question.
|
||||
"""
|
||||
from vibe import configmodel
|
||||
from vibe import configpage
|
||||
from vibe import foldreport as fold
|
||||
|
||||
|
||||
# --------------------------------------------------------------- the fixture
|
||||
|
||||
def _tree(pairs):
|
||||
out = {}
|
||||
for key, value in pairs:
|
||||
node = out
|
||||
parts = key.split('.')
|
||||
for part in parts[:-1]:
|
||||
node = node.setdefault(part, {})
|
||||
node[parts[-1]] = value
|
||||
return out
|
||||
|
||||
|
||||
def _sample(row):
|
||||
"""A type-appropriate value for one SETTINGS row."""
|
||||
kind = row['kind']
|
||||
if kind == 'choice':
|
||||
return (row['choices'] or ('fast',))[0]
|
||||
if kind == 'bool':
|
||||
return True
|
||||
if kind in ('float', 'int'):
|
||||
return 2 if kind == 'int' else 2.5
|
||||
if kind == 'list':
|
||||
return ['ANCHOR_FACE_4T']
|
||||
if kind == 'map':
|
||||
return {'3.3': 'no piece on this job has one'}
|
||||
if kind == 'stages':
|
||||
return [{'name': 'stripping', 'pci_case': 'stripping',
|
||||
'strength': 'fci'}]
|
||||
return 'something'
|
||||
|
||||
|
||||
def _full():
|
||||
return _tree([(row['key'], _sample(row)) for row in configmodel.SETTINGS])
|
||||
|
||||
|
||||
def layers(defaults=None, plant=None, job=None, config=None,
|
||||
plant_exists=True):
|
||||
"""[(label, path, exists, data)] the way `configdump.layer_values` gives
|
||||
it, with the project config layer added only when there is one."""
|
||||
out = [('defaults', None, True, defaults if defaults is not None
|
||||
else _full()),
|
||||
('catalogue', 'C:/tool/catalogue.yaml', True, {}),
|
||||
('plant', 'C:/plant/knowledge.yaml', plant_exists, plant or {}),
|
||||
('job', 'C:/job/knowledge.yaml', True, job or {})]
|
||||
if config is not None:
|
||||
out.append(('config', 'C:/job/sample_job-config.yaml', True, config))
|
||||
return out
|
||||
|
||||
|
||||
def rendered(**over):
|
||||
kwargs = {'title': 'sample_job', 'layers': layers(),
|
||||
'job_path': 'C:/job/knowledge.yaml',
|
||||
'env': [('WILLTOOLS_KNOWLEDGE', '(not set)')],
|
||||
'run_options': {}, 'run_options_path': 'C:/job/run-options.yaml',
|
||||
'locations': {'report_dir': 'C:/job/reports'},
|
||||
'report_dir': 'C:/job/reports',
|
||||
'subtrees': [('parameters', 'the office template',
|
||||
[('member.control_number', 'Control Number',
|
||||
'plant')])],
|
||||
'muted': {}}
|
||||
kwargs.update(over)
|
||||
return configpage.render(**kwargs)
|
||||
|
||||
|
||||
def by_key(sections):
|
||||
return dict((section.key, section) for section in sections)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- the rows
|
||||
|
||||
def test_rows_keep_a_stable_position():
|
||||
rows = configpage.editable_rows(layers())
|
||||
assert [row['n'] for row in rows] == list(range(1, len(rows) + 1))
|
||||
|
||||
|
||||
def test_every_editable_setting_is_on_the_page_exactly_once():
|
||||
rows = configpage.editable_rows(layers())
|
||||
keys = [row['key'] for row in rows]
|
||||
assert keys == [row['key'] for row in configmodel.editable()]
|
||||
assert len(set(keys)) == len(keys)
|
||||
|
||||
|
||||
def test_no_row_is_named_by_its_dotted_key():
|
||||
"""The defect: the button offered `handling.report_dir` as an option name.
|
||||
|
||||
Every SETTINGS row carries a human label and this is what makes sure the
|
||||
page uses it. The key is still on the row -- it is what somebody grepping
|
||||
the YAML needs -- but it is never the name.
|
||||
"""
|
||||
for row in configpage.editable_rows(layers()):
|
||||
assert row['label'], row['key']
|
||||
assert row['label'] != row['key']
|
||||
assert '.' not in row['label'] or ' ' in row['label']
|
||||
|
||||
|
||||
def test_rows_group_under_the_human_section_names():
|
||||
rows = configpage.editable_rows(layers())
|
||||
groups = configpage.group_rows(rows)
|
||||
names = [name for name, _rows in groups]
|
||||
assert names == [name for name in configmodel.sections()
|
||||
if any(row['section'] == name for row in rows)]
|
||||
assert 'Output' in names
|
||||
# A section name is prose, not a path fragment.
|
||||
for name in names:
|
||||
assert '.' not in name
|
||||
|
||||
|
||||
def test_grouping_preserves_the_settings_order():
|
||||
rows = configpage.editable_rows(layers())
|
||||
seen = []
|
||||
for _name, group in configpage.group_rows(rows):
|
||||
seen.extend([row['n'] for row in group])
|
||||
assert seen == sorted(seen)
|
||||
|
||||
|
||||
# ------------------------------------------------------------- value_text()
|
||||
|
||||
def test_an_unknown_value_is_a_sentence_and_never_a_blank():
|
||||
res = configmodel.resolve(layers(defaults={}), 'handling.rig_mode')
|
||||
assert not res['known']
|
||||
assert configpage.value_text('handling.rig_mode', res) == \
|
||||
configpage.NOT_SET
|
||||
assert configpage.value_text('handling.rig_mode', res).strip()
|
||||
|
||||
|
||||
def test_fast_precise_is_shown_the_way_the_config_file_writes_it():
|
||||
tree = _full()
|
||||
tree['handling']['fast_lifter_box'] = False
|
||||
res = configmodel.resolve(layers(defaults=tree),
|
||||
'handling.fast_lifter_box')
|
||||
assert configpage.value_text('handling.fast_lifter_box', res) == 'precise'
|
||||
|
||||
|
||||
# ------------------------------------------------------------- the headline
|
||||
|
||||
def test_the_headline_is_first_and_is_never_folded():
|
||||
sections = rendered()
|
||||
assert sections[0].headline is True
|
||||
assert sections[0].is_open is True
|
||||
assert all(not section.headline for section in sections[1:])
|
||||
|
||||
|
||||
def test_the_headline_is_label_first_with_the_key_as_small_print():
|
||||
sections = rendered()
|
||||
tables = [block for block in sections[0].blocks if block[0] == fold.TABLE]
|
||||
assert tables
|
||||
columns = tables[0][1]
|
||||
assert columns[0] == 'setting'
|
||||
assert columns[-1] == 'key'
|
||||
first = tables[0][2][0]
|
||||
assert first[0] != first[-1] # the label is not the key
|
||||
assert first[-1].startswith('`') # the key is reference small print
|
||||
|
||||
|
||||
def test_every_row_says_where_it_is_changed():
|
||||
"""The column that makes this page the other half of the four-module
|
||||
window instead of a second, competing surface."""
|
||||
sections = rendered(placement={'handling.rig_mode': 'Lifting and handling'})
|
||||
table = [b for b in sections[0].blocks if b[0] == fold.TABLE][0]
|
||||
assert table[1][-2] == 'change it in'
|
||||
wheres = set(row[-2] for row in table[2])
|
||||
assert 'Lifting and handling' in wheres or 'the file' in wheres
|
||||
for row in table[2]:
|
||||
assert row[-2], 'a blank here reads as "nowhere"'
|
||||
|
||||
|
||||
def test_a_setting_no_box_carries_says_it_is_changed_in_the_file():
|
||||
sections = rendered(placement={})
|
||||
table = [b for b in sections[0].blocks if b[0] == fold.TABLE][0]
|
||||
assert set(row[-2] for row in table[2]) == set(['the file'])
|
||||
|
||||
|
||||
def test_without_a_placement_the_column_is_simply_empty():
|
||||
"""The page still renders for a caller that has no window to point at."""
|
||||
sections = rendered()
|
||||
table = [b for b in sections[0].blocks if b[0] == fold.TABLE][0]
|
||||
assert set(row[-2] for row in table[2]) == set([''])
|
||||
|
||||
|
||||
def test_the_headline_says_where_the_edit_goes():
|
||||
text = '\n'.join(block[1] for block in rendered()[0].blocks
|
||||
if block[0] == fold.MD)
|
||||
assert 'C:/job/knowledge.yaml' in text
|
||||
assert 'job layer' in text
|
||||
|
||||
|
||||
def test_a_build_note_reaches_the_top_of_the_page():
|
||||
text = '\n'.join(block[1] for block in
|
||||
rendered(notes=['restart for the full view'])[0].blocks
|
||||
if block[0] == fold.MD)
|
||||
assert 'restart for the full view' in text
|
||||
|
||||
|
||||
# -------------------------------------------------- what opens itself, and why
|
||||
|
||||
def test_nothing_opens_when_nothing_is_wrong():
|
||||
for section in rendered()[1:]:
|
||||
assert section.problem is None, section.key
|
||||
assert section.is_open is False
|
||||
|
||||
|
||||
def test_a_setting_no_layer_sets_opens_its_own_section():
|
||||
tree = _full()
|
||||
del tree['handling']['sling_angle_deg']
|
||||
sections = by_key(rendered(layers=layers(defaults=tree)))
|
||||
assert 'unknown' in sections
|
||||
assert sections['unknown'].is_open
|
||||
assert 'nothing in any layer decides' in sections['unknown'].problem
|
||||
|
||||
|
||||
def test_a_missing_layer_file_opens_the_layers_section():
|
||||
sections = by_key(rendered(layers=layers(plant_exists=False)))
|
||||
assert sections['layers'].is_open
|
||||
assert 'plant' in sections['layers'].problem
|
||||
|
||||
|
||||
def test_a_live_config_file_opens_its_own_section_and_names_what_it_decides():
|
||||
given = layers(config={'handling': {'rig_mode': 'rolling'}})
|
||||
sections = by_key(rendered(layers=given, rows=None,
|
||||
config_path='C:/job/sample_job-config.yaml'))
|
||||
assert sections['config'].is_open
|
||||
assert 'NOT' in sections['config'].problem
|
||||
body = '\n'.join(block[1] for block in sections['config'].blocks
|
||||
if block[0] == fold.MD)
|
||||
assert 'sample_job-config.yaml' in body
|
||||
|
||||
|
||||
def test_a_missing_config_file_says_so_rather_than_falling_back_quietly():
|
||||
sections = by_key(rendered(config_path='C:/gone/config.yaml',
|
||||
config_missing=True))
|
||||
assert 'MISSING' in sections['config'].problem
|
||||
|
||||
|
||||
def test_the_config_chip_marks_the_rows_the_file_decides():
|
||||
given = layers(config={'handling': {'rig_mode': 'rolling'}})
|
||||
rows = configpage.editable_rows(given)
|
||||
marked = configpage.config_rows(rows)
|
||||
assert [row['key'] for row in marked] == ['handling.rig_mode']
|
||||
assert marked[0]['chip'] == '!'
|
||||
|
||||
|
||||
def test_muted_items_open_their_section_and_are_listed_with_the_reason():
|
||||
sections = by_key(rendered(muted={'3.3': 'no piece on this job has one'}))
|
||||
assert sections['muted'].is_open
|
||||
assert '1 checklist item' in sections['muted'].problem
|
||||
rows = [block for block in sections['muted'].blocks
|
||||
if block[0] == fold.TABLE][0][2]
|
||||
assert rows[0][0] == '3.3'
|
||||
assert 'no piece' in rows[0][1]
|
||||
|
||||
|
||||
def test_an_unmapped_parameter_tree_opens_and_says_the_checks_go_quiet():
|
||||
sections = by_key(rendered(subtrees=[('parameters', 'the template', [])]))
|
||||
assert sections['params'].is_open
|
||||
assert 'do not run' in sections['params'].problem
|
||||
|
||||
|
||||
# ------------------------------------------------------------- the read-only
|
||||
|
||||
def test_every_read_only_setting_carries_the_sentence_saying_why():
|
||||
section = by_key(rendered())['readonly']
|
||||
body = '\n'.join(block[1] for block in section.blocks
|
||||
if block[0] == fold.MD)
|
||||
for row in configmodel.read_only():
|
||||
assert row['label'] in body
|
||||
assert (row['why'] or '')[:40] in body
|
||||
|
||||
|
||||
def test_the_read_only_table_is_label_first_with_the_key_alongside():
|
||||
section = by_key(rendered())['readonly']
|
||||
table = [block for block in section.blocks if block[0] == fold.TABLE][0]
|
||||
assert table[1][0] == 'setting'
|
||||
assert table[1][-1] == 'key'
|
||||
for row in table[2]:
|
||||
assert row[0] != row[-1]
|
||||
|
||||
|
||||
# ------------------------------------------------------- one setting up close
|
||||
|
||||
# --------------------------------------------------------- the render itself
|
||||
|
||||
def test_the_whole_page_renders_as_text_with_nothing_folded():
|
||||
body = fold.text(rendered(), show_all=True)
|
||||
assert 'Configuration of sample_job' in body
|
||||
for section in rendered()[1:]:
|
||||
assert section.title.replace('—', '--') in body
|
||||
|
||||
|
||||
def test_the_folded_half_renders_as_one_line_of_html():
|
||||
"""`print_html` is a `print(..., end='')` into the panel's stream and every
|
||||
newline in it starts a new output entry, which would cut a div in half."""
|
||||
body = fold.html(rendered()[1:])
|
||||
assert body
|
||||
assert '\n' not in body
|
||||
|
||||
|
||||
def test_a_pipe_in_a_typed_reason_cannot_split_a_table_row():
|
||||
sections = by_key(rendered(muted={'3.3': 'a | b'}))
|
||||
rows = [block for block in sections['muted'].blocks
|
||||
if block[0] == fold.TABLE][0][2]
|
||||
assert '|' not in rows[0][1]
|
||||
assert '|' in rows[0][1]
|
||||
|
||||
|
||||
def test_the_page_names_the_file_it_was_also_written_to():
|
||||
body = fold.text(rendered(full_path='C:/job/config/last-page.txt'))
|
||||
assert 'last-page.txt' in body
|
||||
|
||||
|
||||
def test_every_section_has_a_summary_so_folded_is_not_gone():
|
||||
for section in rendered()[1:]:
|
||||
assert section.summary or section.problem, section.key
|
||||
|
||||
|
||||
# ------------------------------------------- the muted section, and its seam
|
||||
|
||||
def _mute_rows(*rows):
|
||||
"""`configform.mute_table` output, spelled out rather than imported.
|
||||
|
||||
The page takes these as DATA and never builds them: they need the ticket
|
||||
checklist table, which `configpage` has no business loading. Writing the
|
||||
shape here is what keeps that contract visible.
|
||||
"""
|
||||
out = []
|
||||
for number, name, reason, layer, editable in rows:
|
||||
out.append({'number': number, 'name': name, 'grade': 'A',
|
||||
'known': not name.startswith('('), 'muted': True,
|
||||
'reason': reason, 'layer': layer, 'editable': editable,
|
||||
'why_not': None if editable else 'set by ' + layer,
|
||||
'falls_back': None})
|
||||
return out
|
||||
|
||||
|
||||
def test_the_muted_section_says_where_the_control_is():
|
||||
"""The defect this is for: he went looking for the mute control twice and
|
||||
did not find it, and this page -- the one surface that lists what is
|
||||
muted -- said nothing at all about where muting is done."""
|
||||
section = by_key(rendered(muted={'3.3': 'no piece has one'}))['muted']
|
||||
body = '\n'.join(block[1] for block in section.blocks
|
||||
if block[0] == fold.MD)
|
||||
assert 'Ticket QC' in body
|
||||
assert 'speaker' in body
|
||||
|
||||
|
||||
def test_the_muted_section_says_the_check_still_runs():
|
||||
"""Muting is a volume control, not a switch. A page that showed a mute
|
||||
without saying that would leave the reader believing a check was off."""
|
||||
section = by_key(rendered(muted={'3.3': 'no piece has one'}))['muted']
|
||||
body = '\n'.join(block[1] for block in section.blocks
|
||||
if block[0] == fold.MD)
|
||||
assert 'does not stop a check' in body
|
||||
assert 'never quietened' in body.lower()
|
||||
assert 'still RUNS' in section.problem or 'still' in section.problem
|
||||
|
||||
|
||||
def test_a_mute_this_window_cannot_remove_is_named_with_its_layer():
|
||||
rows = _mute_rows(('3.3', 'release ratio', 'the plant never has one',
|
||||
'plant', False))
|
||||
section = by_key(rendered(muted={'3.3': 'the plant never has one'},
|
||||
muted_rows=rows))['muted']
|
||||
table = [block for block in section.blocks if block[0] == fold.TABLE][0]
|
||||
assert table[1] == ['item', 'checklist row',
|
||||
'why this job never has one', 'set by']
|
||||
assert table[2][0][3] == 'plant'
|
||||
body = '\n'.join(block[1] for block in section.blocks
|
||||
if block[0] == fold.MD)
|
||||
assert 'not the job layer' in body
|
||||
assert 'MERGE' in body
|
||||
|
||||
|
||||
def test_a_muted_number_that_is_no_checklist_item_opens_the_section():
|
||||
"""It mutes nothing, and nothing is what it looks like it did."""
|
||||
rows = _mute_rows(('9.9', '(not a checklist item -- this mutes nothing)',
|
||||
'typed by hand', 'job', True))
|
||||
section = by_key(rendered(muted={'9.9': 'typed by hand'},
|
||||
muted_rows=rows))['muted']
|
||||
assert section.is_open
|
||||
assert 'mute NOTHING' in section.problem
|
||||
|
||||
|
||||
def test_the_muted_section_still_renders_from_the_flat_map_alone():
|
||||
"""Every older caller hands over `muted` and no rows. The section must
|
||||
fall back rather than lose the list."""
|
||||
section = by_key(rendered(muted={'3.3': 'no piece has one'}))['muted']
|
||||
table = [block for block in section.blocks if block[0] == fold.TABLE][0]
|
||||
assert table[1] == ['item', 'why this job never has one']
|
||||
assert table[2][0][0] == '3.3'
|
||||
|
||||
|
||||
# ------------------------------------------------ the chip on a merged map
|
||||
|
||||
def test_a_key_the_layers_merge_is_never_chipped_to_one_layer():
|
||||
"""`resolve` names the last layer that set an entry as though it owned
|
||||
all of them. `J` on a map the plant also writes is the same lie as a
|
||||
silent override, one level down."""
|
||||
given = layers(plant={'checks': {'muted': {'3.3': 'plant'}}},
|
||||
job={'checks': {'muted': {'5.4': 'job'}}})
|
||||
row = [r for r in configpage.editable_rows(given)
|
||||
if r['key'] == 'checks.muted'][0]
|
||||
assert row['chip'] == '+'
|
||||
assert row['res']['layer'] == 'plant + job'
|
||||
assert '3.3 (plant)' in row['value']
|
||||
assert '5.4 (job)' in row['value']
|
||||
|
||||
|
||||
def test_the_legend_explains_the_merged_chip():
|
||||
"""A chip nobody can look up is a chip that reads as a bug."""
|
||||
assert '`+`' in configpage.LEGEND
|
||||
assert 'MERGE' in configpage.LEGEND
|
||||
|
||||
|
||||
def test_a_merged_map_no_layer_sets_still_says_not_set():
|
||||
given = layers(defaults={}, plant={}, job={})
|
||||
row = [r for r in configpage.editable_rows(given)
|
||||
if r['key'] == 'checks.muted'][0]
|
||||
assert row['chip'] == '?'
|
||||
assert row['value'] == configpage.NOT_SET
|
||||
Reference in New Issue
Block a user