Files
willtools-demo/WillTools.extension/tests/test_configmodel.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
2026-09-09 08:32:15 -05:00

876 lines
36 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for vibe.configmodel -- the Revit-free half of the config editor.
Everything the Project Config button does except talking to Revit and drawing
a dialog lives in `configmodel`: which settings exist, what layer each value
came from, whether a value is legal, and the splice that writes one key into a
YAML file without disturbing the thousand lines of comments around it. All of
that is testable here, on host CPython, with no model and no licence -- which
is the point of keeping it out of the button script.
The layer fixtures below are the shape `configdump.layer_values()` returns:
(label, path, exists, data), in merge order.
"""
import pytest
from vibe import configmodel
def layers(defaults=None, catalogue=None, plant=None, job=None, config=None,
missing=()):
rows = [('defaults', None, True, defaults or {}),
('catalogue', 'C:/repo/catalogue.yaml', 'catalogue' not in missing,
catalogue or {}),
('plant', 'C:/plant/knowledge.yaml', 'plant' not in missing,
plant or {}),
('job', 'C:/job/knowledge.yaml', 'job' not in missing, job or {})]
if config is not None:
rows.append(('config', 'C:/job/project-config.yaml', True, config))
return rows
# ------------------------------------------------------------- the inventory
def test_every_setting_is_either_editable_or_says_why_not():
for row in configmodel.SETTINGS:
assert row['edit'] in ('job', 'ro')
if row['edit'] == 'ro':
assert row['why'], '{0} is read-only with no reason'.format(
row['key'])
def test_the_plant_layer_is_not_editable_from_here():
"""The ruling: plant edits do not happen in this button, at all."""
keys = [row['key'] for row in configmodel.editable()]
for plant_only in ('handling.lifter_groups', 'lifters.capacity_lb',
'lifters.catalogue_alias', 'handling.python_exe'):
assert plant_only not in keys
def test_parameters_are_shown_even_though_they_are_not_edited():
assert 'parameters' in [name for name, _note in configmodel.SUBTREES]
def test_the_lifter_factors_of_safety_are_shown_not_edited():
# The paths are `catalogue.safety_factors.*`, not `rigging.*`. `rigging`
# is the shape of panellift's JOB FILE, which liftexport generates -- it
# was never a knowledge path, and while these rows named it they pointed
# at a key no layer declared. Corrected 2026-08-30 after a knowledge/code
# drift audit found it.
#
# AND BOTH ARE NULL as of 2026-08-31, so the `why` has to say null ON
# PURPOSE or the screen reads as an unfilled setting somebody should go
# and fill. The factors are per part now, inside the catalogue entries;
# this pair is only a fallback for hardware the catalogue does not cover.
for key in ('catalogue.safety_factors.tension_fos',
'catalogue.safety_factors.shear_fos'):
row = configmodel.setting(key)
assert row['edit'] == 'ro'
assert 'null on purpose' in row['why'].lower(), row['why']
assert 'fallback' in row['why'].lower(), row['why']
def test_the_factors_of_safety_are_not_offered_under_a_rigging_path():
# Regression: naming a key nothing declares makes the screen report it as
# unset forever, and no edit could ever reach it.
for gone in ('rigging.tension_fos', 'rigging.shear_fos'):
assert configmodel.setting(gone) is None
# ------------------------------------------------------------- provenance
def test_resolve_reports_the_winning_layer_and_the_whole_chain():
res = configmodel.resolve(
layers(defaults={'handling': {'rig_mode': 'rolling'}},
plant={'handling': {'rig_mode': 'static'}}),
'handling.rig_mode')
assert res['value'] == 'static'
assert res['layer'] == 'plant'
assert [label for label, _p, _v in res['chain']] == ['defaults', 'plant']
assert res['overrides'] == [('defaults', 'rolling')]
def test_a_layer_that_restates_a_value_is_not_an_override():
res = configmodel.resolve(
layers(defaults={'handling': {'finish': 'smooth'}},
job={'handling': {'finish': 'smooth'}}), 'handling.finish')
assert res['layer'] == 'job'
assert res['overrides'] == []
def test_a_setting_no_layer_mentions_says_so_rather_than_going_blank():
res = configmodel.resolve(layers(), 'handling.rig_mode')
assert res['known'] is False
assert configmodel.chip(res) == '?'
text = '\n'.join(configmodel.chain_lines(res))
assert 'NOT SET IN ANY LAYER' in text
def test_a_missing_layer_file_is_named_not_silently_skipped():
res = configmodel.resolve(
layers(defaults={'handling': {'finish': 'smooth'}}, missing=('plant',)),
'handling.finish')
assert res['missing'] == ['plant']
assert 'file not present' in '\n'.join(configmodel.chain_lines(res))
def test_the_config_layer_wins_and_the_chain_says_the_edit_will_not_reach():
res = configmodel.resolve(
layers(plant={'handling': {'rig_mode': 'static'}},
config={'handling': {'rig_mode': 'rolling'}}),
'handling.rig_mode')
assert res['layer'] == 'config'
text = '\n'.join(configmodel.chain_lines(res))
assert 'will NOT reach this model' in text
def test_a_read_only_setting_shows_its_reason_instead_of_an_edit_target():
res = configmodel.resolve(layers(), 'handling.python_exe')
text = '\n'.join(configmodel.chain_lines(res))
assert 'NOT EDITABLE HERE' in text
assert 'Editing target' not in text
def test_fast_lifter_box_is_never_shown_as_a_boolean():
assert configmodel.display_value('handling.fast_lifter_box', True) == 'fast'
assert configmodel.display_value('handling.fast_lifter_box',
False) == 'precise'
assert configmodel.to_stored('handling.fast_lifter_box', 'fast') is True
def test_no_value_ever_formats_as_an_empty_string():
for value in (None, '', [], {}, ' '):
assert configmodel.format_value(value).strip()
# ------------------------------------------------------------------- lists
def test_a_list_edit_says_it_replaces_and_names_what_was_replaced():
res = configmodel.resolve(
layers(defaults={'families': {'lifter_include': ['LIFT', 'LPA',
'ANCHOR']}},
plant={'families': {'lifter_include': ['LIFT', 'ANCHOR',
'A1', 'X1']}}),
'families.lifter_include')
text = '\n'.join(configmodel.replace_warning(res))
assert 'LISTS REPLACE' in text
assert 'already REPLACED defaults' in text
assert 'LPA' in text
assert configmodel.start_from(res) == ['LIFT', 'ANCHOR', 'A1', 'X1']
def test_start_from_is_empty_when_nothing_is_in_force():
assert configmodel.start_from(configmodel.resolve(
layers(), 'families.lifter_include')) == []
# -------------------------------------------------------------- entering
@pytest.mark.parametrize('text,expected', [
('3.5', 3.5), ('', None), ('null', None), (' 2 ', 2.0)])
def test_numbers_parse_and_blank_means_null(text, expected):
ok, value, _why = configmodel.parse_input('float', text)
assert ok and value == expected
def test_a_typo_in_a_choice_is_refused_not_guessed():
ok, _value, why = configmodel.parse_input(
'choice', 'static_lift', configmodel.RIG_MODES)
assert not ok and 'static_lift' in why
def test_a_list_is_entered_as_text_and_split_on_commas_and_lines():
ok, value, _why = configmodel.parse_input('list', 'LIFT, ANCHOR\n A1 ,')
assert ok and value == ['LIFT', 'ANCHOR', 'A1']
# ------------------------------------------------------------- validation
@pytest.mark.parametrize('key,value,ok', [
('handling.rig_mode', 'static', True),
('handling.rig_mode', 'static_lift', False),
('handling.group_tolerance_ft', 0.4, True),
('handling.group_tolerance_ft', 0.0, False),
('handling.group_tolerance_ft', None, False),
('handling.sling_angle_deg', 60.0, True),
('handling.sling_angle_deg', 91.0, False),
('handling.prestress_loss_pct', 8.0, True),
('handling.prestress_loss_pct', 100.0, False),
('handling.product_type', 'flat', True),
('handling.product_type', 'flatt', False),
('handling.dunnage.fraction', 0.21, True),
('handling.dunnage.fraction', 0.7, False),
('families.lifter_include', [], False),
('families.lifter_include', ['LIFT'], True),
])
def test_validation(key, value, ok):
assert bool(configmodel.validate(key, value)) is (not ok)
def test_fraction_mode_needs_a_fraction_that_is_already_set():
problems = configmodel.validate('handling.dunnage.mode', 'fraction',
{'handling': {'dunnage': {}}})
assert problems and 'fraction must be set' in problems[0]
assert not configmodel.validate(
'handling.dunnage.mode', 'fraction',
{'handling': {'dunnage': {'fraction': 0.2}}})
def test_a_stage_with_no_multiplier_needs_a_pci_case():
stages = [{'name': 'stripping', 'multiplier': None, 'strength': 'fci'}]
problems = configmodel.validate('handling.stages', stages)
assert any('pci_case' in p for p in problems)
def test_a_stage_naming_an_unknown_insert_group_is_refused():
know = {'handling': {'lifter_groups': {'face': {}}}}
stages = [{'name': 'strip', 'pci_case': 'stripping', 'strength': 'fci',
'groups': ['edge_top']}]
problems = configmodel.validate('handling.stages', stages, know)
assert any('edge_top' in p for p in problems)
def test_no_stage_enabled_is_refused():
stages = [{'name': 'strip', 'pci_case': 'stripping', 'strength': 'fci',
'enabled': False}]
assert any('at least one stage enabled' in p
for p in configmodel.validate('handling.stages', stages))
def test_a_wide_weight_tolerance_warns_but_does_not_refuse():
assert not configmodel.validate('handling.weight_tolerance', 0.08)
assert configmodel.warnings('handling.weight_tolerance', 0.08)
# ------------------------------------------------------------- the splice
JOB = u"""# A job knowledge file, comments and all.
rails:
# this job's left rail is not fixed
left_rail_is_fixed: false
handling:
# Where the reports go. Written by a person, kept by the splice.
report_dir: X:/somewhere/lifting
stages:
- name: stripping
pci_case: stripping
- name: erection
pci_case: yard
# a trailing comment about nothing
"""
def _parse(text):
from vibe import yamlkit
return yamlkit.parse(text)
def test_an_existing_scalar_is_replaced_and_the_comments_survive():
out = configmodel.splice(JOB, 'handling.report_dir', 'Y:/new')
assert _parse(out)['handling']['report_dir'] == 'Y:/new'
assert "this job's left rail is not fixed" in out
assert 'Where the reports go' in out
assert 'a trailing comment about nothing' in out
assert not configmodel.verify(JOB, out, 'handling.report_dir', 'Y:/new')
def test_a_new_key_lands_inside_the_parent_that_already_exists():
out = configmodel.splice(JOB, 'handling.rig_mode', 'static')
tree = _parse(out)
assert tree['handling']['rig_mode'] == 'static'
assert tree['handling']['report_dir'] == 'X:/somewhere/lifting'
assert len(tree['handling']['stages']) == 2
assert not configmodel.verify(JOB, out, 'handling.rig_mode', 'static')
def test_a_new_parent_is_appended_whole():
out = configmodel.splice(JOB, 'families.lifter_include', ['LIFT', 'A1'])
assert _parse(out)['families']['lifter_include'] == ['LIFT', 'A1']
assert not configmodel.verify(JOB, out, 'families.lifter_include',
['LIFT', 'A1'])
def test_a_new_key_three_deep_creates_the_nesting():
out = configmodel.splice(JOB, 'handling.dunnage.mode', 'fraction')
assert _parse(out)['handling']['dunnage']['mode'] == 'fraction'
assert not configmodel.verify(JOB, out, 'handling.dunnage.mode',
'fraction')
def test_replacing_a_block_replaces_the_whole_block():
stages = [{'name': 'strip', 'pci_case': 'stripping', 'rig_mode': 'static'}]
out = configmodel.splice(JOB, 'handling.stages', stages)
tree = _parse(out)
assert len(tree['handling']['stages']) == 1
assert tree['handling']['stages'][0]['rig_mode'] == 'static'
assert tree['handling']['report_dir'] == 'X:/somewhere/lifting'
assert not configmodel.verify(JOB, out, 'handling.stages', stages)
def test_the_note_is_written_above_the_value_and_not_stacked_on_re_edit():
res = configmodel.resolve(
layers(defaults={'handling': {'rig_mode': 'rolling'}}),
'handling.rig_mode')
note = configmodel.note_lines('handling.rig_mode', res, 'this yard strips '
'on fixed slings', when='2026-08-30')
once = configmodel.splice(JOB, 'handling.rig_mode', 'static', note)
assert ' # override: defaults says rolling' in once
assert 'fixed slings' in once
twice = configmodel.splice(once, 'handling.rig_mode', 'rolling', note)
assert twice.count('# override: defaults says rolling') == 1
def test_verify_catches_a_splice_that_changed_something_else():
good = configmodel.splice(JOB, 'handling.report_dir', 'Y:/new')
damaged = good.replace(' - name: erection\n', '')
assert configmodel.verify(JOB, damaged, 'handling.report_dir', 'Y:/new')
def test_verify_catches_an_edit_that_did_not_take():
assert configmodel.verify(JOB, JOB, 'handling.report_dir', 'Y:/new')
def test_the_write_is_refused_when_the_two_readers_disagree():
assert configmodel.reader_disagreement(
{'handling': {'rig_mode': 'static'}},
{'handling': {'rig_mode': 'rolling'}}, 'handling.rig_mode')
assert configmodel.reader_disagreement(
{}, {'handling': {'rig_mode': 'rolling'}}, 'handling.rig_mode')
# pyRevit's reader hands back strings; a string 8.0 and a float 8.0 are
# the same value written two ways, not a disagreement.
assert not configmodel.reader_disagreement(
{'handling': {'prestress_loss_pct': '8.0'}},
{'handling': {'prestress_loss_pct': 8.0}},
'handling.prestress_loss_pct')
# --------------------------------------------------- run-time options
def test_run_options_default_on_and_have_no_layers():
assert configmodel.default_run_options()['expensive_geometry'] is True
assert 'expensive_geometry' not in [row['key']
for row in configmodel.SETTINGS]
def test_run_options_round_trip_and_keep_a_key_from_a_newer_build():
text = configmodel.render_run_options({'expensive_geometry': False,
'something_new': 3})
out = configmodel.read_run_options(text)
assert out['expensive_geometry'] is False
assert out['something_new'] == 3
assert 'NOT knowledge' in text
def test_missing_run_options_file_means_every_default():
assert configmodel.read_run_options(None) == \
configmodel.default_run_options()
def test_a_replaced_key_is_never_left_as_a_second_copy():
"""The bug this caught: nesting level compared against a column count.
`report_dir` sits two spaces in at nesting level 1. Comparing those two
numbers directly says "not found", and the splice then appends a SECOND
`report_dir:` below the first -- a file that reads back correctly through
a last-wins parser and is wrong for a person.
"""
out = configmodel.splice(JOB, 'handling.report_dir', 'Y:/new')
assert out.count('report_dir:') == 1
again = configmodel.splice(out, 'handling.report_dir', 'Z:/newer')
assert again.count('report_dir:') == 1
def test_run_options_round_trip_through_a_real_folder(tmp_path):
root = str(tmp_path)
assert configmodel.load_run_options(root) == \
configmodel.default_run_options()
configmodel.save_run_options(root, {'expensive_geometry': False})
assert configmodel.load_run_options(root)['expensive_geometry'] is False
assert configmodel.run_options_path(root).endswith('run-options.yaml')
# ------------------------------------- the position search, as a run option
def test_the_position_search_is_a_run_option_and_defaults_off():
"""OFF, by the owner's ruling of 2026-08-30 ("searching insert location
还是非常慢"). He re-checks pieces far more often than he asks where the
inserts should have gone, and the search is 93% of the run -- 12.6 s of
a 13.6 s median piece. The report always says when positions were not
computed, so OFF is visible, never silent; switching it on is one toggle
in ProjectConfig for the piece that wants the answer.
(This inverts the original default. The reasoning both ways is in
RUN_OPTIONS' own comment; the owner's usage decided it.)
"""
row = next((o for o in configmodel.RUN_OPTIONS
if o['key'] == 'recommend_positions'), None)
assert row is not None
assert row['default'] is False
def test_the_position_search_option_round_trips(tmp_path):
root = str(tmp_path)
configmodel.save_run_options(root, {'recommend_positions': False})
assert configmodel.load_run_options(root)['recommend_positions'] is False
text = configmodel.render_run_options({'recommend_positions': False})
assert 'recommend_positions' in text
def test_the_position_search_note_says_a_batch_never_runs_it():
"""The option governs the single-piece check only, and somebody reading
the file has to be told that rather than discovering it."""
row = configmodel.run_option('recommend_positions')
assert row is not None
assert 'BATCH' in row['note']
# ==================================================================
# checks.muted -- per-job check muting, typed on the config screen
# ==================================================================
#
# Owner, 2026-08-30: "让我可以注释掉一些check。比如说这个项目里面3.3我就想注释
# 掉。因为所有的panel都不会有这个,一直看着NOT_RUN我也糟心。"
#
# The feature removes rows from a checklist somebody signs, so every guard
# around it is a refusal, not a warning.
def test_a_map_is_typed_one_entry_per_line():
ok, value, why = configmodel.parse_input('map', '3.3 = no piece has one')
assert ok and why is None
assert value == {'3.3': 'no piece has one'}
def test_a_map_accepts_a_colon_and_splits_on_the_first_one_only():
"""The value is a sentence, and a sentence contains colons."""
ok, value, _why = configmodel.parse_input(
'map', '3.3: reason: this job has no release requirement')
assert ok
assert value == {'3.3': 'reason: this job has no release requirement'}
def test_a_map_reads_several_lines():
ok, value, _why = configmodel.parse_input(
'map', '3.3 = never applies\n5.3 = decided by hand here\n')
assert ok
assert value == {'3.3': 'never applies', '5.3': 'decided by hand here'}
def test_a_map_entry_with_no_reason_is_refused_at_the_keyboard():
ok, _value, why = configmodel.parse_input('map', '3.3')
assert not ok
assert 'value is REQUIRED' in why
def test_a_map_entry_with_an_empty_reason_is_refused():
ok, _value, why = configmodel.parse_input('map', '3.3 =')
assert not ok
assert 'no value' in why
def test_a_blank_map_clears_it():
ok, value, _why = configmodel.parse_input('map', '')
assert ok and value == {}
def test_muting_with_a_reason_validates():
assert configmodel.validate(
'checks.muted', {'3.3': 'no piece on this job has one'}) == []
def test_muting_with_no_reason_is_refused():
"""A row that vanished for a reason nobody wrote down reads as a bug."""
out = configmodel.validate('checks.muted', {'3.3': ''})
assert out
assert 'reason is' in out[0] or 'REQUIRED' in out[0]
def test_muting_a_number_that_is_not_a_checklist_item_is_refused():
out = configmodel.validate('checks.muted', {'9.9': 'typo'})
assert out
assert 'not a ticket-checklist item' in out[0]
def test_muting_must_be_a_map_not_a_list():
"""A list would not deep-merge across the layers, and carries no reason."""
out = configmodel.validate('checks.muted', ['3.3'])
assert out
assert 'map' in out[0]
def test_no_muting_at_all_is_fine():
assert configmodel.validate('checks.muted', {}) == []
assert configmodel.validate('checks.muted', None) == []
def test_muting_is_editable_on_the_config_screen():
"""It is a job decision with a reason, so it is made where the note is
written -- not typed into a YAML file with no provenance."""
row = configmodel.setting('checks.muted')
assert row is not None
assert row['edit'] == 'job'
assert row['kind'] == 'map'
assert row in configmodel.editable()
def test_muting_ships_empty():
"""Nothing is muted by default, and empty is the only honest default."""
from vibe import knowledge
assert knowledge.DEFAULTS['checks']['muted'] == {}
# ------------------------------------------------- maps the layers MERGE
#
# `knowledge._deep_update` recurses into a dict and assigns anything else, so a
# LIST replaces and a MAP merges. `resolve` answers for the first shape. Asking
# it about the second is what made the config window show one mute and hide
# another: with the plant muting 3.3 and the job muting 5.4 it answers
# `{'5.4': ...}`, and a screen built on that answer lets somebody "remove" a
# plant mute the merge puts straight back.
def _muted(plant=None, job=None, defaults=None):
def wrap(value):
return {'checks': {'muted': value}} if value is not None else {}
return layers(defaults=wrap(defaults), plant=wrap(plant), job=wrap(job))
def test_a_merged_map_keeps_every_layers_entries():
merged = configmodel.merged_map(
_muted(plant={'3.3': 'the plant never has one'},
job={'5.4': 'no mesh on this job'}), 'checks.muted')
assert sorted(merged) == ['3.3', '5.4']
assert merged['3.3']['layer'] == 'plant'
assert merged['5.4']['layer'] == 'job'
def test_resolve_alone_would_have_hidden_the_plant_entry():
"""The bug, pinned. `resolve` is right about a key that REPLACES and
wrong about one that merges, and the difference is invisible until two
layers have an opinion."""
given = _muted(plant={'3.3': 'plant'}, job={'5.4': 'job'})
assert configmodel.resolve(given, 'checks.muted')['value'] == \
{'5.4': 'job'}
assert sorted(configmodel.merged_map(given, 'checks.muted')) == \
['3.3', '5.4']
def test_a_later_layer_naming_the_same_entry_beats_it_and_says_what_it_beat():
merged = configmodel.merged_map(
_muted(plant={'3.3': 'plant reason'}, job={'3.3': 'job reason'}),
'checks.muted')
assert merged['3.3']['value'] == 'job reason'
assert merged['3.3']['layer'] == 'job'
assert merged['3.3']['overrides'] == [('plant', 'plant reason')]
def test_a_layer_that_sets_the_key_to_something_that_is_not_a_map_replaces():
"""Mirrors `_deep_update`'s sharp edge rather than smoothing it: a list
there really does wipe the map above it, and a merger that pretended
otherwise would describe a file the run does not read."""
merged = configmodel.merged_map(
_muted(plant={'3.3': 'plant'}, job=['5.4']), 'checks.muted')
assert merged == {}
def test_layer_map_is_this_files_own_entries_and_not_the_merge():
"""The splice baseline. Starting from the MERGE would copy the plant's
entries into the job file, where they would stop tracking the plant for
good -- silently, which is the only way this project ever gets hurt."""
given = _muted(plant={'3.3': 'plant'}, job={'5.4': 'job'})
assert configmodel.layer_map(given, 'checks.muted', 'job') == \
{'5.4': 'job'}
assert configmodel.layer_map(given, 'checks.muted', 'plant') == \
{'3.3': 'plant'}
def test_layer_map_is_empty_when_that_layer_never_mentions_the_key():
"""Empty is correct, not missing: `splice` creates a key that is not
there, and an edit has to start from nothing rather than from someone
else's map."""
assert configmodel.layer_map(_muted(plant={'3.3': 'plant'}),
'checks.muted', 'job') == {}
def test_the_chip_for_a_merged_map_never_names_one_layer_as_the_owner():
two = configmodel.merged_map(_muted(plant={'3.3': 'a'}, job={'5.4': 'b'}),
'checks.muted')
one = configmodel.merged_map(_muted(plant={'3.3': 'a'}), 'checks.muted')
assert configmodel.map_chip(two) == '+'
assert configmodel.map_layers(two) == ['plant', 'job']
assert configmodel.map_chip(one) == 'P'
assert configmodel.map_chip({}) == '?'
def test_a_merged_map_formats_with_the_layer_beside_each_entry():
merged = configmodel.merged_map(_muted(plant={'3.3': 'a'},
job={'5.4': 'b'}), 'checks.muted')
assert configmodel.format_merged_map(merged) == \
'2 entries: 3.3 (plant), 5.4 (job)'
assert configmodel.format_merged_map({}) == '(nothing)'
def test_only_the_keys_declared_as_merging_are_treated_that_way():
assert configmodel.merges('checks.muted') is True
assert configmodel.merges('families.lifter_include') is False
# ------------------------------------------------ the note over a merged map
def test_the_note_over_a_merged_map_never_calls_it_an_override():
"""A job entry ADDS to what the plant muted; it does not displace it.
"# override: plant says 2 keys" would be a sentence that is simply
untrue, in the one place a reader has left six months later."""
res = configmodel.resolve(_muted(plant={'3.3': 'a'}), 'checks.muted')
body = '\n'.join(configmodel.note_lines('checks.muted', res, 'because',
when='2026-08-30'))
assert 'override:' not in body
assert 'merged:' in body
assert 'plant sets entries here too' in body
def test_a_merged_map_note_is_not_stacked_on_re_edit():
text = "checks:\n muted:\n '3.3': a\n"
res = configmodel.resolve(_muted(job={'3.3': 'a'}), 'checks.muted')
note = configmodel.note_lines('checks.muted', res, 'first',
when='2026-08-30')
once = configmodel.splice(text, 'checks.muted', {'3.3': 'b'}, note)
twice = configmodel.splice(once, 'checks.muted', {'3.3': 'c'},
configmodel.note_lines('checks.muted', res,
'second',
when='2026-08-30'))
assert twice.count('# merged:') == once.count('# merged:')
assert 'first' not in twice
def test_the_continuation_note_line_is_recognised_and_not_stacked():
"""`# (defaults said X)` was not in `_NOTE`, so the upward walk stopped on
it and every re-edit of a key more than one layer had touched stacked
another block of notes on the last -- leaving the OLDEST, the wrong one,
readable at the top."""
given = layers(defaults={'handling': {'rig_mode': 'rolling'}},
plant={'handling': {'rig_mode': 'static'}})
res = configmodel.resolve(given, 'handling.rig_mode')
assert res['overrides'] == [('defaults', 'rolling')]
text = 'handling:\n rig_mode: static\n'
once = configmodel.splice(text, 'handling.rig_mode', 'rolling',
configmodel.note_lines('handling.rig_mode', res,
'first',
when='2026-08-30'))
twice = configmodel.splice(once, 'handling.rig_mode', 'static',
configmodel.note_lines('handling.rig_mode', res,
'second',
when='2026-08-30'))
assert twice.count('(defaults said rolling)') == 1
assert 'first' not in twice
def test_a_merged_map_splices_and_verifies_like_any_other_key():
text = "job:\n name: sample_job\n\nchecks:\n muted:\n '3.3': a\n"
after = configmodel.splice(text, 'checks.muted',
{'3.3': 'a', '5.4': 'b'}, ['# note'])
assert configmodel.verify(text, after, 'checks.muted',
{'3.3': 'a', '5.4': 'b'}) == []
assert 'sample_job' in after
# ------------------------------------- one reader seeing less than the other
WRAPPED = """job:
name: sample_job
details:
marks_seen:
parts: [P76, P89,
SP29, SP30]
snapshot:
detached_dir: null
"""
def test_a_reader_that_stops_early_is_reported_as_that_and_not_as_one_key():
"""THE DEFECT. The owner set `snapshot.detached_dir` from the window and
was told "the two YAML readers disagree about snapshot.detached_dir. This
file has to be edited by hand." Both halves were true and neither was
useful: nothing is wrong with that key.
What had happened is that the editor's reader STOPPED READING at a `[...]`
list wrapped over two lines, and `snapshot` -- three sections further down
-- was simply not in its tree. Every key past the cut would have produced
the same message and sent him to the same wrong place.
"""
theirs = {'job': {'name': 'sample_job'},
'details': {'marks_seen': {'parts': ['P76']}},
'snapshot': {'detached_dir': None}}
ours = {'job': {'name': 'sample_job'}}
body = '\n'.join(configmodel.reader_coverage(theirs, ours, WRAPPED))
assert 'cannot see all of this file' in body
assert 'snapshot' in body
assert 'NOTHING HAS BEEN WRITTEN' in body
def test_the_coverage_check_names_the_wrapped_list_as_the_likely_cause():
body = '\n'.join(configmodel.reader_coverage(
{'snapshot': {'detached_dir': None}}, {}, WRAPPED))
assert 'more than one line' in body
assert 'line 6' in body # `parts: [P76, P89,`
def test_a_wrapped_flow_list_is_found_and_a_closed_one_is_not():
assert configmodel.wrapped_flow_lines(
'a:\n parts: [P76, P89,\n SP29]\n') == [2]
assert configmodel.wrapped_flow_lines('a:\n parts: [P76, P89]\n') == []
# A comment is not a finding: nothing reads it.
assert configmodel.wrapped_flow_lines('# parts: [P76,\n') == []
def test_two_readers_that_see_the_same_file_report_nothing():
same = {'snapshot': {'detached_dir': None}, 'job': {'name': 'sample_job'}}
assert configmodel.reader_coverage(same, dict(same), WRAPPED) == []
def test_the_coverage_check_says_the_cause_is_unknown_rather_than_guessing():
"""It scans for ONE shape. A file that stops being readable for another
reason must not be told it has a wrapped list in it."""
body = '\n'.join(configmodel.reader_coverage(
{'snapshot': {'detached_dir': None}}, {}, 'job:\n name: x\n'))
assert 'has to be found by hand' in body
assert 'more than one line' not in body
# ============================================================================
# `checks.not_applicable` / `checks.only_applies_to` -- refused at the door
# ============================================================================
#
# Belt and braces with the resolver, exactly as `checks.muted` is: a
# knowledge file may be edited by hand and never come through this screen, so
# the run-time side refuses the same shapes and prints the refusal on the
# report. Both halves have to exist, because either one alone lets somebody
# believe a safety check is off while it is on.
def _entry(**over):
got = {'applies': False,
'not_applicable_because': 'no piece here has one',
'not_applicable_since': '2026-08-31'}
got.update(over)
return got
def test_a_good_declaration_is_accepted():
assert configmodel.validate(
'checks.not_applicable', {'3.3': {'any': _entry()}}) == []
def test_a_bare_list_is_refused_because_it_cannot_merge():
said = configmodel.validate('checks.not_applicable', ['3.3'])
assert said and 'DELETE' in said[0]
def test_a_number_that_is_not_a_checklist_item_is_refused():
said = configmodel.validate('checks.not_applicable',
{'9.9': {'any': _entry()}})
assert any('not a ticket-checklist item' in s for s in said)
def test_a_reasonless_or_dateless_declaration_is_refused():
assert configmodel.validate(
'checks.not_applicable',
{'3.3': {'any': _entry(not_applicable_because='')}})
assert configmodel.validate(
'checks.not_applicable',
{'3.3': {'any': _entry(not_applicable_since='soon')}})
def test_a_condition_naming_no_declared_panel_type_is_refused():
"""It would match nothing, ever, and matching nothing is exactly what it
would look like it did."""
know = {'panel_types': {'types': {'insulated': {}, 'solid': {}}}}
said = configmodel.validate('checks.not_applicable',
{'3.3': {'sandwich': _entry()}}, know)
assert any('no declared panel type' in s for s in said)
assert configmodel.validate('checks.not_applicable',
{'3.3': {'insulated': _entry()}}, know) == []
# `any` is always legal: it is the whole-plant axis, not a type.
assert configmodel.validate('checks.not_applicable',
{'3.3': {'any': _entry()}}, know) == []
def test_scoping_a_row_to_any_is_refused_at_the_door_too():
said = configmodel.validate('checks.only_applies_to', {'3.3': {'any': {
'scoped': True, 'scoped_because': 'x', 'scoped_since': '2026-08-31'}}})
assert any('not a scope' in s for s in said)
def test_a_scope_needs_its_own_reason_and_date():
said = configmodel.validate('checks.only_applies_to', {'3.3': {
'insulated': {'scoped': True}}})
assert said and 'REQUIRED' in said[0]
# --------------------------------- which FILE the plant/public layer reads
def test_knowledge_source_keys_are_the_two_the_owner_named():
keys = [key for key, _label in configmodel.KNOWLEDGE_SOURCE_KEYS]
assert keys == ['knowledge_sources.plant_file',
'knowledge_sources.public_file']
def test_a_missing_file_is_refused_never_written_quietly(tmp_path):
"""CLAUDE.md's rule for `_config_missing` / `_plant_missing`, carried to
the per-job pointer this window writes: a chosen file that is not there
must be a loud, named refusal."""
gone = str(tmp_path / 'nope.yaml')
for key in ('knowledge_sources.plant_file', 'knowledge_sources.public_file'):
said = configmodel.validate(key, gone)
assert said, key
assert repr(gone) in said[0]
assert 'does not exist' in said[0]
def test_a_real_file_is_accepted(tmp_path):
real = tmp_path / 'plant.yaml'
real.write_text(u'lifters: {}\n', encoding='utf-8')
for key in ('knowledge_sources.plant_file', 'knowledge_sources.public_file'):
assert configmodel.validate(key, str(real)) == []
def test_a_blank_value_is_never_refused_it_means_go_back_to_default():
for key in ('knowledge_sources.plant_file', 'knowledge_sources.public_file'):
assert configmodel.validate(key, None) == []
assert configmodel.validate(key, '') == []
def test_knowledge_source_problems_is_silent_when_never_set():
"""No layer mentions the key -- every install before this feature
existed. Not a problem."""
assert configmodel.knowledge_source_problems(layers()) == []
def test_knowledge_source_problems_is_silent_when_explicitly_cleared():
"""`known=True` with a `None` value is a decision made in the window --
distinct from `known=False`, and neither is a problem."""
job = {'knowledge_sources': {'plant_file': None}}
problems = configmodel.knowledge_source_problems(layers(job=job))
assert problems == []
def test_knowledge_source_problems_names_a_pointer_that_is_gone():
job = {'knowledge_sources': {'plant_file': 'C:/nowhere/knowledge.yaml'}}
problems = configmodel.knowledge_source_problems(layers(job=job))
assert len(problems) == 1
key, label, path = problems[0]
assert key == 'knowledge_sources.plant_file'
assert label == 'Plant knowledge file'
assert path == 'C:/nowhere/knowledge.yaml'
def test_knowledge_source_problems_is_silent_when_the_file_is_really_there(
tmp_path):
real = tmp_path / 'catalogue.yaml'
real.write_text(u'inserts: {}\n', encoding='utf-8')
job = {'knowledge_sources': {'public_file': str(real)}}
assert configmodel.knowledge_source_problems(layers(job=job)) == []