# -*- 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 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 '') + 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