Files
willtools-demo/WillTools.extension/tests/test_configform.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

1295 lines
54 KiB
Python

# -*- coding: utf-8 -*-
"""The four-module Project Config window: its mapping, its fields, its XAML.
`vibe.configform` is everything about that window that does not need Revit:
which setting belongs in which of the four modules, what kind of control each
one deserves, what one click of a toggle means, and the mute rules. The window
itself is `ProjectConfig.pushbutton/` (found by `_button()` below, because the
ribbon moves) -- a `forms.WPFWindow`
and a XAML file -- and IT CANNOT BE OPENED ON A HOST MACHINE. So this suite
does the three things a host machine can:
1. asserts the mapping is total (`test_every_editable_setting_is_placed`),
which is the check that stops a setting added to `configmodel` from
shipping with nowhere on screen to change it;
2. asserts the field/control rules, including the two the owner asked for
by name -- a rig mode that is a button showing `static` and becomes
`rolling`, and a sling angle that is a box you type in;
3. parses `ProjectConfig.xaml` and `script.py` together and fails if they
have drifted -- every `Click=` handler in the XAML exists as a method,
and every `x:Name` the script looks up exists as an element. `wpf.Load
Component` binds those by name at load time, so a drift here is a
window that throws the moment it opens, on the owner's machine, with
nothing on a host machine having noticed.
WHAT ONLY A PRESS CAN PROVE, and is therefore not asserted anywhere here:
* that the XAML renders at all in Revit's WPF (a bad attribute value is a
load-time exception, not a parse error);
* that `script.load_ui` finds `ProjectConfig.xaml` in the bundle;
* that a click actually reaches `toggle_click` -- the handler wiring done
in code (`button.Click += ...`) is not visible to this suite;
* that the window is modal over Revit and that the output page comes to the
front after it closes.
"""
import ast
import io
import os
import re
import pytest
from vibe import configform
from vibe import configmodel
from vibe import plantpath
EXTENSION = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _button():
"""Where the button folder is, found rather than spelled out.
The ribbon is folder structure, and folder structure moves: this button
has already been through `AI.tab/QC.panel/`, `WillTools.tab/
Reinforcing.panel/setup.stack/` and now `WillTools.tab/Setup.panel/
Config.pulldown/` in two days. A hard-coded path turns a ribbon
reorganisation into a failure in a test about XAML, which sends the next
person looking in the wrong file. This walk cost nothing and has now
absorbed three moves without an edit.
"""
for root, dirs, _files in os.walk(EXTENSION):
for name in dirs:
if name == 'ProjectConfig.pushbutton':
return os.path.join(root, name)
raise AssertionError(
'ProjectConfig.pushbutton is nowhere under {0}'.format(EXTENSION))
BUTTON = _button()
XAML = os.path.join(BUTTON, 'ProjectConfig.xaml')
SCRIPT = os.path.join(BUTTON, 'script.py')
# --------------------------------------------------------------- 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):
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 {}
if kind == 'stages':
return [{'name': 'stripping', 'pci_case': 'stripping',
'strength': 'fci', 'rig_mode': 'static'},
{'name': 'yard', 'pci_case': 'yard', 'strength': 'fc',
'enabled': False}]
return 'something'
def layers(job=None, plant=None):
defaults = _tree([(row['key'], _sample(row))
for row in configmodel.SETTINGS])
defaults.setdefault('scope', {})['mark_prefixes'] = ['PNL']
defaults.setdefault('tolerances', {})['dimension_in'] = 0.375
return [('defaults', None, True, defaults),
('catalogue', 'C:/tool/catalogue.yaml', True, {}),
('plant', 'C:/plant/knowledge.yaml', True, plant or {}),
('job', 'C:/job/knowledge.yaml', True, job or {})]
def rows(job=None, plant=None, run_options=None, info=None):
return configform.resolve_all(configform.fields(info=info),
layers(job=job, plant=plant),
run_options or {})
def row_for(key, **kw):
for row in rows(**kw):
if row['key'] == key:
return row
return None
# ------------------------------------------------------------- the mapping
def test_every_editable_setting_is_placed_or_explicitly_left_out():
"""The contract that makes the not-shown pool honest.
A setting added to `configmodel.SETTINGS` as editable must land in one of
the four modules or in `NOT_SHOWN` with the reason it is not a control.
Failing here is the intended behaviour: somebody has to decide.
"""
assert configform.unmapped() == [], (
'these editable settings are in no module and have no not-shown '
'reason: {0}. Put each one in configform.MODULE_OF or in '
'configform.NOT_SHOWN with the sentence saying why a person does not '
'change it from this window.'.format(configform.unmapped()))
def test_the_four_modules_are_the_owners_four():
assert [key for key, _t, _n in configform.MODULES] == [
'file_path', 'lifting', 'ticketqc', 'job_knowledge']
def test_nothing_is_in_two_modules_at_once():
seen = {}
for field in configform.fields():
assert field['key'] not in seen or seen[field['key']] == \
field['module'], field['key']
seen[field['key']] = field['module']
def test_no_read_only_setting_reaches_the_window():
"""Plant and catalogue values are not in this window at all -- one plant
edit reaches every job, so it stays a deliberate act somewhere else."""
keys = set(field['key'] for field in configform.fields())
for row in configmodel.read_only():
assert row['key'] not in keys, row['key']
def test_every_not_shown_setting_carries_a_reason():
for label, key, why in configform.not_shown_rows():
assert why and len(why) > 20, key
assert label
def test_the_not_shown_pool_is_not_empty():
"""`剩下的都不在这里显示` -- if everything is shown, the design is gone."""
assert len(configform.not_shown_rows()) >= 8
# ---------------------------------------------------------------- the fields
def test_rig_mode_is_a_two_state_button_not_a_list():
"""The owner's example: a button reading `static` that becomes `rolling`."""
field = row_for('handling.rig_mode')
assert field['kind'] == 'toggle'
assert set(field['values']) == set(('rolling', 'static'))
assert configform.next_value(field, 'static') == 'rolling'
assert configform.next_value(field, 'rolling') == 'static'
def test_sling_angle_is_a_box_you_type_in():
field = row_for('handling.sling_angle_deg')
assert field['kind'] == 'number'
ok, value, _why = configform.parse(field, '52.5')
assert ok and value == 52.5
def test_a_number_box_refuses_a_word_rather_than_guessing():
field = row_for('handling.sling_angle_deg')
ok, _value, why = configform.parse(field, 'sixty')
assert not ok
assert 'not a number' in why
def test_a_blank_number_box_is_null_not_zero():
field = row_for('handling.dunnage.fraction')
ok, value, _why = configform.parse(field, ' ')
assert ok and value is None
def test_dunnage_mode_cycles_through_blank():
field = row_for('handling.dunnage.mode')
assert field['kind'] == 'toggle'
assert None in field['values']
seen = []
value = None
for _ in range(3):
value = configform.next_value(field, value)
seen.append(value)
assert seen == ['lifter_rows', 'fraction', None]
def test_a_toggle_on_a_value_no_layer_set_starts_at_the_top():
field = row_for('nomenclature.ticket_unit')
assert configform.next_value(field, 'something nobody wrote') == \
field['values'][0]
def test_run_options_are_toggles_and_say_they_have_no_layers():
field = row_for('recommend_positions')
assert field['source'] == 'run'
assert field['kind'] == 'toggle'
assert 'no layers' in field['layer']
assert configform.next_value(field, True) is False
def test_the_two_lifting_run_options_are_the_ones_the_owner_named():
lifting = [f['key'] for f in configform.run_fields()
if f['module'] == configform.LIFTING]
assert sorted(lifting) == ['expensive_geometry', 'recommend_positions']
def test_a_list_field_is_a_box_and_splits_on_commas():
field = row_for('scope.mark_prefixes')
assert field['kind'] == 'list'
ok, value, _why = configform.parse(field, 'PNL, BM ,')
assert ok and value == ['PNL', 'BM']
def test_a_path_field_keeps_a_share_path_exactly_as_typed():
field = row_for('handling.report_dir')
ok, value, _why = configform.parse(field, r'\\share\jobs\out')
assert ok and value == r'\\share\jobs\out'
def test_the_chip_says_which_layer_a_value_came_from():
field = row_for('handling.rig_mode',
job={'handling': {'rig_mode': 'rolling'}})
assert field['chip'] == 'J'
assert field['layer'] == 'job'
assert field['display'] == 'rolling'
def test_a_value_no_layer_sets_says_so_rather_than_showing_blank():
field = row_for('snapshot.detached_dir')
assert field['chip'] == '?'
assert field['display'] == '(not set)'
def test_every_field_carries_a_human_label_not_a_dotted_key():
for field in rows():
if field['source'] == 'info':
continue
assert field['label'], field['key']
assert field['label'] != field['key'], field['key']
def test_derived_paths_are_shown_and_not_editable():
given = [(configform.FILE_PATH, 'Job data folder', 'C:/job/_ticketqc',
'follows the model')]
field = [f for f in rows(info=given) if f['kind'] == 'info'][0]
assert field['source'] == 'info'
assert field['display'] == 'C:/job/_ticketqc'
assert field['chip'] == ''
def test_modules_come_back_in_the_owners_order_with_their_rows():
grouped = configform.by_module(rows())
assert [key for key, _t, _n, _r in grouped] == [
'file_path', 'lifting', 'ticketqc', 'job_knowledge']
for _key, _title, _note, group in grouped:
assert group, 'a module with no fields is a box with nothing in it'
# ---------------------------------------------------------------- the stages
def test_a_stage_row_is_its_name_its_rig_mode_and_whether_it_runs():
field = row_for('handling.stages')
assert field['kind'] == 'stages'
listed = configform.stage_rows(field['value'])
assert listed[0][1] == 'stripping'
assert listed[0][2] == 'static'
assert listed[0][3] is True
assert listed[1][3] is False
def test_changing_one_stage_hands_back_every_stage():
"""`handling.stages` is one list and lists replace: a write of one cell
writes all of them, so the edit is made against the whole list."""
field = row_for('handling.stages')
after = configform.set_stage(field['value'], 0, 'rig_mode', 'rolling')
assert len(after) == len(field['value'])
assert after[0]['rig_mode'] == 'rolling'
assert after[1] == field['value'][1]
assert field['value'][0]['rig_mode'] == 'static' # the original stands
def test_a_stage_with_no_rig_mode_shows_blank_not_an_empty_button():
assert configform.stage_display(None) == configform.BLANK
# ----------------------------------------------------------------- the mutes
def test_a_mute_without_a_reason_is_refused():
"""Enforced here and not only in configmodel.validate, because the loaded
build may predate that rule."""
ok, mutes, why = configform.add_mute({}, '3.3', ' ')
assert not ok
assert mutes == {}
assert 'REQUIRED' in why
def test_a_mute_without_a_number_is_refused():
ok, _mutes, why = configform.add_mute({}, '', 'because')
assert not ok
assert 'checklist item' in why
def test_a_mute_lands_with_its_reason_and_can_be_removed():
ok, mutes, _why = configform.add_mute({}, ' 3.3 ', ' no piece has one ',
when='2026-08-30')
assert ok
assert mutes == {'3.3': 'no piece has one (Project Config, 2026-08-30)'}
ok, mutes, _why = configform.remove_mute(mutes, '3.3')
assert ok and mutes == {}
# ------------------------------------------------- the reason, and its date
def test_the_reason_is_dated_for_you_and_the_sentence_is_still_yours():
"""The owner's own mutes read `"... (Will 2026-08-30)"`. The date is the
tool's job; the sentence is never written, defaulted or suggested."""
stamped = configform.stamp_reason('WWF does not exist on this job',
when='2026-08-30')
assert stamped.startswith('WWF does not exist on this job')
assert stamped.endswith('(Project Config, 2026-08-30)')
def test_a_reason_that_is_already_dated_is_not_dated_twice():
"""Re-muting an item must not stack `(Project Config, 08-30) (Project
Config, 08-31)` on the end of a sentence somebody wrote once."""
once = configform.stamp_reason('no piece has one', when='2026-08-30')
twice = configform.stamp_reason(once, when='2026-08-30')
assert twice == once
assert configform.stamp_reason('his own words (Will 2026-08-30)') == \
'his own words (Will 2026-08-30)'
def test_a_blank_reason_gets_no_date_and_stays_blank():
"""A date on its own is not a reason. If the stamp were added first, an
empty box would produce `(Project Config, 2026-08-30)` -- a mute that
looks documented and says nothing."""
assert configform.stamp_reason(' ') == ''
ok, mutes, why = configform.add_mute({}, '3.3', ' ')
assert not ok and mutes == {} and 'REQUIRED' in why
def test_nothing_in_this_module_offers_a_default_reason():
"""No placeholder, no 'muted via UI', no pre-filled sentence anywhere. A
reason somebody accepted without reading is the same as no reason on the
day it has to be argued with."""
ok, _mutes, _why = configform.add_mute({}, '3.3', 'because')
assert ok
# The reason box is created empty and the only thing ever put into it is
# what the person has already typed. Asserted on the assignment itself
# rather than by hunting for banned phrases in the file: this file is
# mostly comments, and a comment saying "no placeholder" would fail a
# phrase search while a real placeholder three lines down would not.
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'box.Text = self.mute_draft' in source
for other in re.findall(r'\.Text\s*=\s*([^\n]+)', source):
assert 'reason' not in other.lower() or 'draft' in other, other
def test_a_number_that_is_not_a_checklist_item_is_refused_at_the_click():
"""`configmodel.validate` catches this at Save. Catching it at the click
is better: a number that mutes nothing is how somebody believes a row is
off while it is on, and the sooner that is said the less of it is typed."""
known = configform.checklist_numbers()
ok, mutes, why = configform.add_mute({}, '9.9', 'because', known=known)
assert not ok and mutes == {}
assert 'mute nothing' in why
def test_a_caller_that_could_not_read_the_table_takes_the_number_on_trust():
"""`known=None` is a real answer -- see `checklist_items()`. Refusing
every mute because a module would not load is worse than accepting one."""
ok, mutes, _why = configform.add_mute({}, '9.9', 'because', known=None,
when='2026-08-30')
assert ok and '9.9' in mutes
# --------------------------------------------- the checklist, and whose mute
def test_the_rows_come_from_the_real_checklist_table_not_a_copy():
"""A HAND-KEPT SECOND LIST IS THE 5.3 DEFECT. `CHECKLIST_FOR_ENGINEER`
was a tuple holding `('5.5',)`, 5.3 was never added, and 5.3 printed a
red NOT RUN on every ticket until somebody spent a day on it. A copy
here would fail worse: a row this window never draws is a row nobody can
mute, and nothing anywhere would say why."""
from vibe import ticketpage
listed = [number for number, _n, _g in configform.checklist_items()]
assert listed == [item[0] for item in ticketpage.CHECKLIST_ITEMS]
assert len(listed) > 30
def test_every_checklist_row_is_offered_even_when_nothing_is_muted():
rows = configform.mute_table({}, {})
assert len(rows) == len(configform.checklist_items())
assert all(row['editable'] and not row['muted'] for row in rows)
assert configform.mute_summary(rows).endswith(
'Every row reports at full volume.')
def test_a_job_mute_is_this_windows_to_remove():
merged = {'5.4': {'value': 'no mesh here', 'layer': 'job',
'path': 'C:/job/knowledge.yaml', 'overrides': []}}
row = _mute_row_for('5.4', merged, {'5.4': 'no mesh here'})
assert row['muted'] and row['editable']
assert row['layer'] == 'job'
assert row['why_not'] is None
assert row['name'] # the checklist's own words, not the number
def test_a_plant_mute_is_shown_marked_and_not_removable_here():
"""The window writes the job layer, and the layers MERGE this map -- so a
job entry cannot take a plant mute away. A Remove button there would look
like it worked and change nothing, which is the failure this whole file
is against."""
merged = {'3.3': {'value': 'the plant never has one', 'layer': 'plant',
'path': 'C:/plant/knowledge.yaml', 'overrides': []}}
row = _mute_row_for('3.3', merged, {})
assert row['muted']
assert row['editable'] is False
assert 'plant' in row['why_not']
assert 'MERGE' in row['why_not']
assert row['reason'] == 'the plant never has one'
def test_a_plant_mute_is_never_absent_from_the_list():
"""Absent would be worse than not-editable: the report would go on
hiding a checklist row with nothing on this screen saying so."""
merged = {'3.3': {'value': 'plant', 'layer': 'plant', 'path': None,
'overrides': []}}
rows = configform.mute_table(merged, {})
assert [row['number'] for row in rows if row['muted']] == ['3.3']
assert '1 of the' in configform.mute_summary(rows)
assert 'cannot be changed here' in configform.mute_summary(rows)
def test_unmuting_a_job_mute_that_sits_on_a_plant_one_says_it_falls_back():
"""Removing the job entry does not unmute the item: the plant's entry
takes over. Saying "Unmute" and leaving it muted is the same lie in the
other direction."""
merged = {'3.3': {'value': 'this job', 'layer': 'job', 'path': None,
'overrides': [('plant', 'the plant never has one')]}}
row = _mute_row_for('3.3', merged, {'3.3': 'this job'})
assert row['editable'] is True
assert row['falls_back']
assert 'plant' in row['falls_back']
assert 'stays muted' in row['falls_back']
def test_a_muted_number_that_is_no_checklist_item_is_listed_and_says_so():
"""It mutes nothing, and nothing is what it looks like it did. A job file
edited by hand can carry one, so the screen has to show it rather than
quietly drop a line somebody wrote."""
merged = {'9.9': {'value': 'typed by hand', 'layer': 'job', 'path': None,
'overrides': []}}
rows = configform.mute_table(merged, {'9.9': 'typed by hand'})
odd = [row for row in rows if row['number'] == '9.9'][0]
assert odd is rows[-1] # after every real row, never in place
assert odd['known'] is False
assert 'mutes nothing' in odd['name']
def _mute_row_for(number, merged, job_mutes):
for row in configform.mute_table(merged, job_mutes):
if row['number'] == number:
return row
raise AssertionError('{0} is not in the mute table'.format(number))
# ------------------------------------------ what the window reads and writes
def test_the_mute_field_reads_the_merge_and_writes_the_job_layer_only():
"""THE BUG THIS FIXES. `configmodel.resolve` answers with the WINNING
layer's whole map, which for a key the layers merge is not what the run
reads: with the plant muting 3.3 and the job muting 5.4 it answers
`{'5.4': ...}`, so the screen showed one mute and hid the other.
The row now carries both halves and they are different things: `merged`
is what a run reads, `value` is what this file holds and is the only
thing a splice may replace."""
field = row_for('checks.muted',
job={'checks': {'muted': {'5.4': 'no mesh'}}},
plant={'checks': {'muted': {'3.3': 'no release rule'}}})
assert sorted(field['merged']) == ['3.3', '5.4']
assert field['merged']['3.3']['layer'] == 'plant'
assert field['merged']['5.4']['layer'] == 'job'
assert field['value'] == {'5.4': 'no mesh'}
def test_the_mute_chip_says_two_layers_rather_than_naming_one():
field = row_for('checks.muted',
job={'checks': {'muted': {'5.4': 'a'}}},
plant={'checks': {'muted': {'3.3': 'b'}}})
assert field['chip'] == '+'
assert field['layer'] == 'plant + job'
assert field['display'] == '2 muted (plant + job)'
def test_the_mute_display_survives_the_plain_map_a_pending_change_carries():
"""Two shapes reach `display`: the merged map with its layers, and this
file's own {number: reason}. The second is what `change_lines` and the
summary printed after the window closes are handed, where a traceback
has nowhere to go."""
field = row_for('checks.muted')
assert configform.display(field, {'3.3': 'because'}) == '1 muted: 3.3'
assert configform.display(field, {}) == 'nothing muted'
# ---------------------------------------------------- the key that is muted
def test_the_psi_tolerance_is_declared_inert_not_a_box_you_can_type_in():
"""Owner's ruling on drift D1: "不要删掉,要'静音'掉。静音配置应该在 config
里面。" Nothing compares a psi against this key, so a number box would be
a control that decides nothing -- and whoever typed in it would believe a
check had got stricter."""
field = row_for('tolerances.psi')
assert field['kind'] == 'inert'
assert field['source'] == 'inert'
assert field['module'] == configform.TICKETQC
assert 'MUTED, NOT DELETED' in field['note']
assert 'MUTED' in field['display']
def test_muting_a_key_is_not_muting_a_checklist_row():
"""They are different objects and must not be run together. A checklist
mute changes what a run reports, is a job fact and is the engineer's to
set -- so it is a control. An inert key changes no run on any job, so it
is a declaration."""
inert = row_for('tolerances.psi')
mutes = row_for('checks.muted')
assert inert['source'] == 'inert' and mutes['source'] == 'job'
assert inert['key'] not in configform.placement()
assert configform.placement()['checks.muted'] == 'Ticket QC'
def test_an_inert_key_is_still_shown_with_whatever_the_file_says():
"""Kept, not deleted -- so the number in the file stays visible. Hiding
it would make the ruling unreadable from the screen that records it."""
field = row_for('tolerances.psi',
job={'tolerances': {'psi': 50.0}})
assert '50.0' in field['display']
assert field['chip'] == 'J'
def test_removing_something_that_is_not_muted_says_so():
ok, _mutes, why = configform.remove_mute({'3.3': 'x'}, '9.9')
assert not ok and '9.9' in why
def test_the_mute_list_reads_back_sorted_with_its_reasons():
assert configform.mute_rows({'5.1': 'b', '3.3': 'a'}) == \
[('3.3', 'a'), ('5.1', 'b')]
def test_muting_does_not_mutate_the_map_it_was_given():
before = {'3.3': 'a'}
configform.add_mute(before, '4.1', 'b')
assert before == {'3.3': 'a'}
# ------------------------------------------------------------ the edit rules
def test_a_toggle_clicked_back_to_where_it_started_is_not_a_change():
assert configform.same('rolling', 'rolling') is True
def test_true_and_one_are_not_the_same_setting():
"""`1 == True` in Python. A write that turned a count into a boolean
because they compared equal is a file nobody asked for."""
assert configform.same(1, True) is False
assert configform.same(True, 1) is False
def test_change_lines_read_label_first_with_the_key_last():
field = row_for('handling.rig_mode')
line = configform.change_lines([(field, 'static', 'rolling')])[0]
assert line.startswith('Lift type')
assert line.endswith('(handling.rig_mode)')
def test_a_file_url_survives_a_space_in_the_job_folder():
"""Markdown ends a link at the first space, and job folders have spaces."""
url = configform.file_url(r'C:\My Job\_ticketqc\knowledge.yaml')
assert url == 'file:///C:/My%20Job/_ticketqc/knowledge.yaml'
assert ' ' not in url
# ------------------------------------------------------ the window and the XAML
def _xaml():
return io.open(XAML, encoding='utf-8').read()
def _script_ast():
return ast.parse(io.open(SCRIPT, encoding='utf-8').read())
def _window_class():
for node in _script_ast().body:
if isinstance(node, ast.ClassDef) and node.name == 'ConfigWindow':
return node
raise AssertionError('ConfigWindow is gone from script.py')
def _methods():
return set(node.name for node in _window_class().body
if isinstance(node, ast.FunctionDef))
def test_the_xaml_is_well_formed_xml():
from xml.etree import ElementTree
ElementTree.fromstring(_xaml().encode('utf-8'))
def test_every_click_handler_named_in_the_xaml_exists_on_the_window():
"""`wpf.LoadComponent` binds these by name when the window loads, so a
missing one is an exception the moment the owner presses the button."""
named = set(re.findall(r'Click="([A-Za-z_][A-Za-z0-9_]*)"', _xaml()))
assert named, 'the window has no buttons any more'
missing = sorted(named - _methods())
assert not missing, 'XAML calls {0}, ConfigWindow has no such method'.format(
missing)
def test_every_element_the_script_looks_up_is_named_in_the_xaml():
names = set(re.findall(r'x:Name="([A-Za-z_][A-Za-z0-9_]*)"', _xaml()))
source = io.open(SCRIPT, encoding='utf-8').read()
for wanted in ('header', 'subheader', 'status', 'reason_box', 'rest_line',
'panel_file_path', 'panel_lifting', 'panel_ticketqc',
'panel_job_knowledge', 'group_file_path', 'group_lifting',
'group_ticketqc', 'group_job_knowledge'):
assert wanted in names, '{0} is not x:Name-d in the XAML'.format(wanted)
assert 'self.' + wanted in source, \
'{0} is named in the XAML and never used'.format(wanted)
def test_there_is_a_panel_for_each_module():
names = set(re.findall(r'x:Name="([A-Za-z_][A-Za-z0-9_]*)"', _xaml()))
for key, _title, _note in configform.MODULES:
assert 'panel_' + key in names
assert 'group_' + key in names
def test_the_window_has_a_save_and_a_way_out_to_the_file():
body = _xaml()
assert 'save_click' in body
assert 'openfile_click' in body
assert 'rest_line' in body
def test_the_header_wraps_because_that_is_the_defect_being_fixed():
"""Defect 1 was a non-wrapping Label in a fixed-height row. Every prose
element in this window must say TextWrapping."""
body = _xaml()
for name in ('header', 'subheader', 'status', 'rest_line'):
chunk = body.split('x:Name="{0}"'.format(name))[1].split('/>')[0]
assert 'TextWrapping="Wrap"' in chunk, name
def test_the_script_has_no_module_docstring():
"""The ribbon carries no hover text; documentation is in `#` comments."""
assert ast.get_docstring(_script_ast()) is None
@pytest.mark.parametrize('handler', ['toggle_click', 'stage_click',
'mute_start_click', 'mute_add_click',
'mute_cancel_click', 'mute_remove_click',
'mute_reason_changed',
'text_changed', 'save_click',
'cancel_click', 'openfile_click',
'fullview_click', 'section_toggled'])
def test_the_window_still_has_its_handlers(handler):
assert handler in _methods()
# ------------------------------------------------ every module folds, 2026-09-01
#
# "project config现在有点儿太复杂了。。。给弄成每个部分都可以折叠的吧。需要
# 调啥,再把那个部分给打开,调整,完事儿。" -- every module folds, and folded
# is the default. What a host machine can prove: the four modules are built
# from `Expander`, not `GroupBox`; every one starts folded absent session
# memory; a module carrying a live override says so on its own header before
# it is ever opened; and the fold/unfold handler is wired in `script.py` the
# same way every other control's handler is.
def test_the_four_modules_are_expanders_not_groupboxes():
body = _xaml()
for key, _title, _note in configform.MODULES:
chunk = body[body.index('x:Name="group_{0}"'.format(key)) - 20:]
chunk = chunk[:chunk.index('>') + 1]
assert '<Expander' in chunk, (key, chunk)
assert '<GroupBox x:Name="group_' not in body
def test_every_module_starts_collapsed_in_the_xaml():
"""The fallback for the very first paint of a fresh session -- before
`script.py` ever gets to override it from remembered state."""
body = _xaml()
for key, _title, _note in configform.MODULES:
chunk = body[body.index('x:Name="group_{0}"'.format(key)):]
chunk = chunk[:chunk.index('>') + 1]
assert 'IsExpanded="False"' in chunk, key
def test_the_fold_handler_is_wired_like_every_other_control():
source = io.open(SCRIPT, encoding='utf-8').read()
assert '.Expanded += self.section_toggled' in source
assert '.Collapsed += self.section_toggled' in source
def test_the_header_is_built_before_the_fold_state():
"""Ordering, not just presence: a header set after `IsExpanded` would
still be correct on screen (WPF does not care), but a reader of this
file six months from now should not have to check that -- the header
always reflects what is ABOUT to be folded, never what a click just
revealed."""
source = io.open(SCRIPT, encoding='utf-8').read()
header_at = source.index('box.Header = ')
expanded_at = source.index('box.IsExpanded = ')
assert header_at < expanded_at
# ---- `is_modified` / `modified_count` -- the fact a folded header states
def _row(kind='number', source='job', chip='D', is_set=None):
row = {'kind': kind, 'source': source, 'chip': chip}
if is_set is not None:
row['is_set'] = is_set
return row
def test_a_row_still_on_the_shipped_default_is_not_modified():
assert configform.is_modified(_row(chip='D')) is False
def test_a_row_no_layer_knows_is_not_modified():
"""Not known and not overridden are the same non-event for this
purpose, even though `configmodel.chip` prints them differently
elsewhere: neither is a decision a folded header has to surface."""
assert configform.is_modified(_row(chip='?')) is False
@pytest.mark.parametrize('chip', ['J', 'P', '!', '+', '*'])
def test_a_row_a_real_layer_set_is_modified(chip):
assert configform.is_modified(_row(chip=chip)) is True
def test_an_info_row_is_never_modified():
"""A derived path is a fact about where the tool put something, not a
decision -- see the `info` kind's own docstring at the top of this
module."""
assert configform.is_modified(_row(kind='info', chip='J')) is False
def test_an_inert_row_is_never_modified():
"""Declared-and-not-wired: whatever the file says under it changes no
run, so it is not the kind of override a folded header exists to
surface -- see the `inert` kind's own docstring."""
assert configform.is_modified(_row(kind='inert', chip='J')) is False
def test_a_run_option_is_modified_only_when_the_file_actually_names_it():
assert configform.is_modified(
_row(source='run', chip='-', is_set=False)) is False
assert configform.is_modified(
_row(source='run', chip='-', is_set=True)) is True
def test_modified_count_counts_only_the_modified_ones():
group = [_row(chip='D'), _row(chip='J'), _row(chip='P'),
_row(kind='info', chip='J'), _row(chip='?')]
assert configform.modified_count(group) == 2
# ---- session-only expansion memory
def test_a_module_is_collapsed_the_first_time_this_session_asks():
assert configform.remembered_expanded('a module nothing has touched') \
is False
def test_remembering_an_expansion_is_read_back():
configform.remember_expanded(configform.LIFTING, True)
try:
assert configform.remembered_expanded(configform.LIFTING) is True
finally:
configform.remember_expanded(configform.LIFTING, False)
def test_forgetting_an_expansion_folds_it_again():
configform.remember_expanded(configform.TICKETQC, True)
configform.remember_expanded(configform.TICKETQC, False)
assert configform.remembered_expanded(configform.TICKETQC) is False
# ------------------------------------------- what muting means, and the icon
def test_muting_does_not_stop_the_check_and_every_surface_says_so():
"""The owner's spec, 2026-08-30: the check "还是偷偷跑" -- it still runs --
and only its presentation changes. That is a CHANGE OF MEANING from what
muting used to be, so every sentence a person can read has to carry it.
An old sentence saying a muted row "leaves the checklist table" would now
be false, and a false sentence about muting is the exact thing muting was
introduced to stop."""
from vibe import configmodel as CM
surfaces = [CM.setting('checks.muted')['note'],
configform.PRESENT['checks.muted']['note'],
dict((k, n) for k, _t, n in configform.MODULES)['ticketqc']]
for body in surfaces:
assert 'run' in body.lower(), body[:60]
# And none of them may still claim the row goes away.
assert 'leaves the checklist' not in body, body[:60]
joined = ' '.join(surfaces).lower()
assert 'never quietened' in joined or 'never removes a fail' in joined
def test_a_failure_is_never_quietened_and_the_notes_still_say_so():
from vibe import configmodel as CM
body = (CM.setting('checks.muted')['note'] + ' ' +
configform.PRESENT['checks.muted']['note']).upper()
assert 'FAILURE IS NEVER QUIETENED' in body
def test_the_muted_state_is_its_own_word_and_not_a_colour_on_not_run():
"""A row that ran and passed quietly, and a row that could not run at
all, are different answers. If muted-and-fine were NOT RUN painted grey
then a real NOT RUN -- a defect in the tooling -- would be one styling
rule away from invisible."""
assert configform.MUTED_STATE
assert configform.MUTED_STATE != 'NOT RUN'
assert configform.MUTED_STATE not in ('OK', 'WARN', 'FAIL',
'FOR ENGINEER')
def test_the_muted_state_seam_with_ticketpage():
"""One word, two modules. `ticketpage` renders the checklist and this
file sets the mute, so the window would promise a word the report does
not print if they drifted.
THE SEAM IS NOW BUILT, and this test did its job on the way: while
`ticketpage` had no such constant it asserted the OTHER side -- that
`apply_muting` still took the row off the table -- precisely so it
would fire the day muting landed. It fired. What it holds now is the
equality it was always written for."""
from vibe import ticketpage
word = getattr(ticketpage, configform.MUTED_STATE_ATTR, None)
assert word is not None, (
'ticketpage has no {0}. The window sets a mute the report cannot '
'name.'.format(configform.MUTED_STATE_ATTR))
assert word == configform.MUTED_STATE, (
'ticketpage.{0} is {1!r} and configform.MUTED_STATE is {2!r}. '
'The window promises a word the report does not print.'.format(
configform.MUTED_STATE_ATTR, word, configform.MUTED_STATE))
# And the renderer really uses it: a muted row keeps its place and goes
# grey, rather than leaving the table the way it used to.
kept, notes = ticketpage.apply_muting(
[('3.3', "f'ci/f'c release ratio in band", 'B', 'NOT RUN', 'x')],
{'3.3': 'no release rule on this job'})
assert [row[0] for row in kept] == ['3.3']
assert kept[0][3] == configform.MUTED_STATE
assert ticketpage.state_color(kept[0][3]) == 'grey'
assert notes
def test_a_failure_survives_muting_in_the_renderer_as_it_always_has():
"""The invariant, pinned from this side too: whatever muting comes to
mean, a FAIL is never taken off the table."""
from vibe import ticketpage
kept, notes = ticketpage.apply_muting(
[('3.3', 'release ratio', 'B', 'FAIL', 'it is 0.4')],
{'3.3': 'no release rule on this job'})
assert [row[0] for row in kept] == ['3.3']
assert any('FAIL' in (note[3] or '').upper() or
'fail' in (note[3] or '') for note in notes)
def test_the_control_is_the_windows_volume_icon_he_asked_for():
""""就是 windows 的那个音量图标" -- Segoe MDL2 Assets, the two code points
the Windows volume flyout itself draws. BMP, so no surrogate pair, and
the font ships with the OS."""
assert configform.SPEAKER_FONT == 'Segoe MDL2 Assets'
assert configform.SPEAKER_ON == u''
assert configform.SPEAKER_MUTED == u''
assert len(configform.SPEAKER_ON) == 1 # no surrogate pair
assert len(configform.SPEAKER_MUTED) == 1
assert configform.SPEAKER_ON != configform.SPEAKER_MUTED
def test_the_speaker_says_what_the_click_will_do_not_what_the_state_is():
"""The state is the glyph and the colour. What a speaker cannot say on
its own is the thing people get wrong -- whether it stops the check."""
assert 'still runs' in configform.SPEAKER_HINT[False]
assert 'Unmute' in configform.SPEAKER_HINT[True]
def test_the_window_uses_the_icon_and_sets_the_font_explicitly():
"""With the window's default family those code points are private-use
and render as an empty box, and an empty box beside every checklist row
is worse than a word."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'configform.SPEAKER_MUTED' in source
assert 'configform.SPEAKER_ON' in source
assert 'configform.SPEAKER_FONT' in source
assert 'FontFamily' in source
# ------------------------------------------------------- no Chinese on screen
def _cjk(text):
return bool(re.search(u'[一-鿿 -〿＀-￯]', text))
def test_no_chinese_reaches_the_window():
""""另外 UI 里面不要有中文。啥玩意儿啊这是?" -- 2026-08-30, on finding a
`RUN_OPTIONS` note quoting a ruling in Chinese under a control.
A `note` is not a comment: `configform.run_fields()` and
`settings_fields()` copy it straight onto the screen under its control.
So the rule is a test, like the no-hover-text one: the verbatim Chinese
belongs in a `#` comment, which nothing renders, and the sentence on
screen is in English.
"""
from vibe import configmodel as CM
bad = []
for row in CM.SETTINGS:
for part in (row['label'], row.get('note'), row.get('why')):
if part and _cjk(part):
bad.append(('configmodel.SETTINGS', row['key']))
for row in CM.RUN_OPTIONS:
for part in (row['label'], row.get('note')):
if part and _cjk(part):
bad.append(('configmodel.RUN_OPTIONS', row['key']))
for field in configform.fields():
for part in (field.get('label'), field.get('note')):
if part and _cjk(part):
bad.append(('configform field', field['key']))
for _key, title, note in configform.MODULES:
if _cjk(title) or _cjk(note):
bad.append(('configform.MODULES', title))
for _label, key, why in configform.not_shown_rows():
if _cjk(why):
bad.append(('configform.NOT_SHOWN', key))
assert bad == [], (
'these strings are drawn in the Project Config window and carry '
'Chinese: {0}. Move the verbatim quote into a `#` comment and put '
'English on the screen.'.format(bad))
def test_no_chinese_in_the_xaml_or_in_a_string_the_window_prints():
"""The window's own chrome and every sentence it builds. Comments and
docstrings are exempt: pyRevit renders neither, and the owner's verbatim
words are the provenance this repo keeps everywhere."""
assert not _cjk(_xaml())
tree = _script_ast()
literals = []
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str) and _cjk(node.value):
literals.append((node.lineno, node.value[:40]))
docstrings = set()
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.Module)):
body = getattr(node, 'body', None) or []
if body and isinstance(body[0], ast.Expr) and \
isinstance(body[0].value, ast.Constant) and \
isinstance(body[0].value.value, str):
docstrings.add(body[0].value.lineno)
leaked = [row for row in literals if row[0] not in docstrings]
assert leaked == [], (
'these string literals in the window carry Chinese and are not '
'docstrings, so they can reach the screen: {0}'.format(leaked))
def test_the_window_draws_the_checklist_rather_than_asking_for_a_number():
"""THE DEFECT. He looked for this control twice and did not find it
twice, because it was a 60-pixel box you typed a checklist number into
under a heading called "Muted checklist items". A control you can only
work if you already know the answer is not a control. So the window
builds its rows from the real table and never asks for a number."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'configform.mute_table' in source
assert 'configform.mute_summary' in source
# The old add-row: a number box and an Add button, both gone.
assert 'self.mute_item' not in source
assert "Content = 'Add'" not in source
def test_the_window_writes_the_job_layer_of_the_mute_map_and_not_the_merge():
"""`self.mutes` is the job layer's own map -- the only thing a splice may
replace. Handing the MERGE to `remember` would copy the plant's entries
into the job file, where they would stop tracking the plant for good."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'configmodel.layer_map' in source or \
"row['value']" in source
assert 'self.remember(self.rows[\'checks.muted\'], dict(self.mutes))' in \
source
def test_the_mute_list_and_the_full_view_read_the_same_rows():
"""One table, two surfaces. The page's `set by` column and the window's
buttons cannot disagree about whose mute an entry is if they are built
from the same call."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert source.count('configform.mute_table') >= 2
def test_the_script_never_prints_while_a_window_is_open():
"""Defect 2. A modal dialog closing re-activates Revit's main window and
the output page goes behind it, so nothing may be printed before the
dialog returns -- `output.print_*` must not appear inside ConfigWindow.
"""
for node in ast.walk(_window_class()):
if not isinstance(node, ast.Attribute):
continue
value = getattr(node, 'value', None)
if isinstance(value, ast.Name) and value.id == 'output':
assert not node.attr.startswith('print'), (
'ConfigWindow calls output.{0} -- the page must not be '
'written while the modal window is up.'.format(node.attr))
# ------------------------------------- which FILE the plant/public layer reads
def test_the_two_knowledge_source_pickers_are_in_job_knowledge():
plant = row_for('knowledge_sources.plant_file')
public = row_for('knowledge_sources.public_file')
assert plant['module'] == configform.JOB_KNOWLEDGE
assert public['module'] == configform.JOB_KNOWLEDGE
assert plant['kind'] == 'filepick'
assert public['kind'] == 'filepick'
assert plant['source'] == 'job'
assert public['source'] == 'job'
assert plant['label'] and plant['label'] != plant['key']
assert public['label'] and public['label'] != public['key']
def test_a_filepick_field_parses_like_a_path():
"""Blank means "go back to the default discovery", the same as any other
path field -- Browse only ever fills the same box, so a chosen file and a
typed one are parsed one way."""
field = row_for('knowledge_sources.plant_file')
ok, value, _why = configform.parse(field, ' ')
assert ok and value is None
ok, value, _why = configform.parse(field, r'C:\plant\knowledge.yaml')
assert ok and value == r'C:\plant\knowledge.yaml'
def test_a_filepick_field_not_known_anywhere_says_not_set():
field = row_for('knowledge_sources.public_file')
assert field['chip'] == '?'
assert field['display'] == '(not set)'
def test_a_filepick_field_the_job_sets_shows_the_path_and_the_job_chip():
field = row_for('knowledge_sources.plant_file',
job={'knowledge_sources': {
'plant_file': 'C:/plant/knowledge.yaml'}})
assert field['chip'] == 'J'
assert field['display'] == 'C:/plant/knowledge.yaml'
# --------------------------------------------------- never set vs cleared
def test_knowledge_source_state_never_set():
res = {'known': False, 'value': None, 'layer': None}
said = configform.knowledge_source_state(res)
assert 'Never set' in said
def test_knowledge_source_state_cleared_is_not_the_same_sentence_as_never_set():
"""EMPTY AND ABSENT ARE DIFFERENT STATES. A job file that states the key
blank is a decision made in this window; a job file that never mentions
it is every install before this feature existed."""
cleared = configform.knowledge_source_state(
{'known': True, 'value': None, 'layer': 'job'})
never = configform.knowledge_source_state(
{'known': False, 'value': None, 'layer': None})
assert cleared != never
assert 'Cleared' in cleared
assert 'Never set' in never
def test_knowledge_source_state_set_names_the_layer_and_the_path():
said = configform.knowledge_source_state(
{'known': True, 'value': 'C:/plant/knowledge.yaml', 'layer': 'job'})
assert 'job' in said
assert 'C:/plant/knowledge.yaml' in said
# ------------------------------------------------- the window: Browse wiring
def test_the_window_offers_a_browse_button_filtered_to_yaml():
source = io.open(SCRIPT, encoding='utf-8').read()
assert "kind == 'filepick'" in source
assert 'filepick_for' in source
assert 'filepick_click' in source
assert "forms.pick_file" in source
assert "file_ext='yaml'" in source
def test_browse_writes_into_the_same_box_save_reads():
"""Browse is a shortcut for typing, not a second mechanism -- the chosen
path has to reach `self.typed`/`self.controls` the same way a keystroke
does, or the missing-file refusal and the empty-vs-absent split would
have two code paths to keep in step instead of one."""
source = io.open(SCRIPT, encoding='utf-8').read()
click = source[source.index('def filepick_click'):
source.index('def toggle_click')]
assert 'box.Text = chosen' in click
assert 'self.typed[key] = chosen' in click
def test_the_window_warns_loudly_on_a_missing_knowledge_source_pointer():
"""`build_notes` is what the window shows before anybody has touched a
single control -- the loud, named refusal for a pointer that has gone
missing since it was last set has to be there, not only inside `Save`."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'configmodel.knowledge_source_problems' in source
def test_the_window_shows_what_is_really_being_read_right_now():
"""`knowledge.sources(doc)` is the ground truth for where the plant and
public layers are read from today, independent of the picker's own
write-only state -- see `configmodel.KNOWLEDGE_SOURCE_KEYS`."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'knowledge.sources(doc)' in source
assert 'knowledge_source_info_rows' in source
# --------------------------------------------- job.plant: the plant dropdown
#
# Gitea will/bridge issue #2. `knowledge.detect_plant` resolves a plant in
# three levels and the owner's ruling is that when all three miss, the
# engineer picks the plant ONCE, here, and it is written to the job layer as
# `job.plant`.
def test_auto_detect_is_not_a_usable_plant_id():
"""The sentinel must never collide with a real plant folder name --
`plantpath._PLANT_ID_RE` requires the first character to be alphanumeric,
which `(` is not, but this is asserted directly rather than trusted."""
assert not plantpath.valid_plant_id(configform.AUTO_DETECT)
def test_plant_options_lists_auto_detect_first_then_the_plant_folders(
tmp_path, monkeypatch):
monkeypatch.setenv(plantpath.PLANT_ENV, str(tmp_path))
for name in ('alpha', 'bravo'):
(tmp_path / name).mkdir()
(tmp_path / name / 'knowledge.yaml').write_text(u'plant:\n id: %s\n'
% name)
assert configform.plant_options() == (configform.AUTO_DETECT, 'alpha',
'bravo')
def test_plant_options_is_read_fresh_not_cached(tmp_path, monkeypatch):
"""A plant folder can be added mid-session (this file, like every other
`vibe` module, is held by pyRevit's engine until Revit restarts), so the
options list must be recomputed every call rather than baked into a
module-level tuple at import time."""
monkeypatch.setenv(plantpath.PLANT_ENV, str(tmp_path))
assert configform.plant_options() == (configform.AUTO_DETECT,)
(tmp_path / 'charlie').mkdir()
(tmp_path / 'charlie' / 'knowledge.yaml').write_text(u'plant:\n id: c\n')
assert configform.plant_options() == (configform.AUTO_DETECT, 'charlie')
def test_job_plant_is_a_combo_in_job_knowledge():
field = row_for('job.plant')
assert field is not None
assert field['module'] == configform.JOB_KNOWLEDGE
assert field['kind'] == 'combo'
assert field['source'] == 'job'
assert field['label'] and field['label'] != field['key']
def test_job_plant_not_known_anywhere_says_not_set():
field = row_for('job.plant')
assert field['chip'] == '?'
assert field['display'] == '(not set)'
def test_job_plant_set_by_the_job_shows_the_id_and_the_job_chip():
field = row_for('job.plant', job={'job': {'plant': 'northgate'}})
assert field['chip'] == 'J'
assert field['value'] == 'northgate'
assert field['display'] == 'northgate'
def test_job_plant_cleared_is_known_but_none_and_reads_as_auto_detect():
"""EMPTY AND ABSENT ARE DIFFERENT STATES, the same distinction
`knowledge_source_state` keeps for the filepick rows: a job file that
explicitly names `job.plant: null` is a decision (auto-detect chosen on
purpose), not silence."""
field = row_for('job.plant', job={'job': {'plant': None}})
assert field['chip'] == 'J'
assert field['value'] is None
assert field['display'] == configform.AUTO_DETECT
def test_display_combo_none_is_auto_detect_and_known_true_by_default():
"""`change_lines` (the pending-edit summary) calls `display` with its
default `known=True`, so a pending edit TO auto-detect must render as the
word, never as `(not set)` -- that word means something else entirely
(nothing decides this, anywhere)."""
field = {'kind': 'combo', 'source': 'job'}
assert configform.display(field, None) == configform.AUTO_DETECT
assert configform.display(field, 'northgate') == 'northgate'
def test_job_plant_is_not_a_configmodel_setting():
"""`job.plant` is EXTRA, like the two knowledge_sources.* pickers --
`configmodel.SETTINGS` carries no row for it, and `unmapped()` /
`not_shown_rows()` (which only inspect `configmodel.editable()`) must
keep passing without ever mentioning it."""
assert configmodel.setting('job.plant') is None
assert 'job.plant' not in configform.unmapped()
assert 'job.plant' not in [key for _l, key, _w in configform.not_shown_rows()]
def test_job_plant_validate_blank_is_fine():
assert configmodel.validate('job.plant', None, {}) == []
def test_job_plant_validate_refuses_a_malformed_id():
said = configmodel.validate('job.plant', 'north/gate', {})
assert said and 'folder name' in said[0]
def test_job_plant_validate_refuses_a_plant_with_no_folder(tmp_path,
monkeypatch):
monkeypatch.setenv(plantpath.PLANT_ENV, str(tmp_path))
said = configmodel.validate('job.plant', 'nosuchplant', {})
assert said and 'not a plant folder' in said[0]
def test_job_plant_validate_accepts_a_real_plant_folder(tmp_path, monkeypatch):
monkeypatch.setenv(plantpath.PLANT_ENV, str(tmp_path))
(tmp_path / 'northgate').mkdir()
(tmp_path / 'northgate' / 'knowledge.yaml').write_text(
u'plant:\n id: northgate\n')
assert configmodel.validate('job.plant', 'northgate', {}) == []
def test_the_window_builds_a_real_dropdown_not_a_toggle_for_the_plant():
"""A plant list is arbitrarily long, so `job.plant` must not reuse
`toggle_for`'s click-to-cycle control -- a real `ComboBox`, built fresh
from `configform.plant_options()`."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert "kind == 'combo'" in source
assert 'combo_for' in source
assert 'combo_changed' in source
assert 'configform.plant_options()' in source
assert 'C.ComboBox()' in source
def test_the_window_shows_the_current_plant_choice_beside_the_dropdown():
"""`knowledge.plant_choice(doc)` -- the SAME three-level resolution the
dropdown is about to change -- is read and shown beside it, so a person
sees how the job is recognised today before touching the control."""
source = io.open(SCRIPT, encoding='utf-8').read()
assert 'plant_choice_line' in source
assert 'knowledge.plant_choice(doc)' in source