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

History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
"""Tests for `vibe.specsave` -- where an extracted spec is copied to, and what
the button says is still unresolved in it.
The module is deliberately Revit-free (paths and text in, paths and text out),
so all of it is reachable from host CPython through the conftest trick. What
is worth testing here is not that it formats a string: it is the three rules
that would be invisible if they broke --
* the file name box has to MEAN something for a single design, and the
folder has to be what is used for several;
* a `file:///` href with forward slashes, because a bare Windows path is a
dead link in pyRevit's output panel and a dead link looks exactly like a
live one;
* a blank field must NOT force a fold open, or every section opens on every
press and the fold's contract is worth nothing.
"""
import io
import os
import re
from vibe import specsave
# ----------------------------------------------------------------- file names
def test_safe_name_keeps_a_design_number_readable():
assert specsave.safe_name('PNL1.0') == 'PNL1.0'
assert specsave.safe_name('WP 12-A') == 'WP 12-A'
def test_safe_name_replaces_what_a_path_cannot_hold():
assert specsave.safe_name('PNL/1') == 'PNL_1'
assert specsave.safe_name('') == 'unnamed'
def test_default_name_is_the_design_when_there_is_one():
assert specsave.default_name(['PNL1']) == 'PNL1.yaml'
def test_default_name_counts_when_there_are_several():
assert specsave.default_name(['PNL1', 'PNL2', 'PNL3']) == \
'design-extraction-3-designs.yaml'
def test_default_name_survives_an_empty_list():
assert specsave.default_name([]) == 'design-extraction.yaml'
# --------------------------------------------------------------- where it goes
def test_one_design_is_written_to_exactly_the_path_that_was_typed():
# The name box has to mean something. A dialog whose file name is quietly
# ignored is the trap CLAUDE.md names about the config file.
got = specsave.targets(r'C:\jobs\my-reading.yaml', ['PNL1'])
assert got == [('PNL1', r'C:\jobs\my-reading.yaml')]
def test_several_designs_take_the_folder_and_keep_their_own_names():
got = specsave.targets(os.path.join('C:', 'jobs', 'whatever.yaml'),
['PNL1', 'PNL2'])
assert [d for d, _p in got] == ['PNL1', 'PNL2']
assert [os.path.basename(p) for _d, p in got] == ['PNL1.yaml', 'PNL2.yaml']
assert set(os.path.dirname(p) for _d, p in got) == \
{os.path.join('C:', 'jobs')}
def test_several_designs_keep_the_extension_that_was_chosen():
got = specsave.targets(os.path.join('C:', 'jobs', 'x.txt'),
['PNL1', 'PNL2'])
assert [os.path.basename(p) for _d, p in got] == ['PNL1.txt', 'PNL2.txt']
def test_no_path_means_no_targets():
assert specsave.targets('', ['PNL1']) == []
assert specsave.targets('C:/x.yaml', []) == []
# --------------------------------------------------------------------- links
def test_href_is_a_url_not_a_windows_path():
# This is the bug the fix exists for: backslashes and no scheme render as
# a link and navigate nowhere.
assert specsave.href(r'C:\jobs\PNL1.yaml') == 'file:///C:/jobs/PNL1.yaml'
def test_same_file_spots_a_save_onto_the_original(tmp_path):
# The dialog opens on the folder the specs already live in, so pressing
# Save without moving is an ordinary accident. It must not be reported as
# a copy to somewhere else.
one = str(tmp_path / 'PNL1.yaml')
assert specsave.same_file(one, one)
# The same file reached the other way round the separator.
assert specsave.same_file(one, one.replace('\\', '/'))
assert not specsave.same_file(one, str(tmp_path / 'other' / 'PNL1.yaml'))
def test_no_path_is_never_the_same_file():
assert not specsave.same_file('', '')
assert not specsave.same_file('C:/x.yaml', '')
def test_link_shows_the_real_path_and_hrefs_the_url():
got = specsave.link(r'C:\jobs\PNL1.yaml')
assert got == r'[C:\jobs\PNL1.yaml](file:///C:/jobs/PNL1.yaml)'
# The visible half keeps the backslashes -- that is what gets copied into
# Explorer.
assert got.split('](')[0] == r'[C:\jobs\PNL1.yaml'
# ------------------------------------------------------- remembering a folder
def test_recall_is_empty_when_nothing_was_ever_saved(tmp_path):
assert specsave.recall_dir(str(tmp_path)) == ''
def test_remember_then_recall(tmp_path):
folder = tmp_path / 'somewhere'
folder.mkdir()
specsave.remember_dir(str(tmp_path), str(folder))
assert specsave.recall_dir(str(tmp_path)) == str(folder)
def test_a_pointer_to_a_folder_that_is_gone_is_dropped(tmp_path):
# Seeding a dialog with a path that no longer exists is how a file lands
# somewhere nobody meant.
pointer = tmp_path / specsave.POINTER
with io.open(str(pointer), 'w', encoding='utf-8') as fh:
fh.write(u'Z:\\not\\here')
assert specsave.recall_dir(str(tmp_path)) == ''
def test_remembering_never_raises(tmp_path):
assert specsave.remember_dir(str(tmp_path / 'no-such-dir'), 'C:\\x') == ''
assert specsave.remember_dir(str(tmp_path), '') == ''
# ---------------------------------------------------------------- unresolved
def test_unresolved_entries_read_text_and_reason():
spec = {'unresolved': [{'text': 'USE MC STANDARD', 'reason': 'no rule'}]}
assert specsave.unresolved_entries(spec) == [('USE MC STANDARD', 'no rule')]
def test_a_bare_string_entry_still_counts():
# An older extractor could write a plain string. The one list whose job is
# not losing lines must not lose a line to its own shape.
assert specsave.unresolved_entries({'unresolved': ['SOMETHING']}) == \
[('SOMETHING', '')]
def test_entries_with_no_text_are_dropped():
spec = {'unresolved': [{'reason': 'why'}, {'text': '', 'reason': 'x'}]}
assert specsave.unresolved_entries(spec) == []
def test_no_unresolved_section_at_all_is_not_an_error():
assert specsave.unresolved_entries({}) == []
assert specsave.unresolved_entries({'unresolved': None}) == []
def test_shared_reason_when_every_note_gives_the_same_one():
entries = [('a', 'no rule matched'), ('b', 'no rule matched')]
assert specsave.shared_reason(entries) == 'no rule matched'
def test_no_shared_reason_when_they_differ():
assert specsave.shared_reason([('a', 'one'), ('b', 'two')]) == ''
assert specsave.shared_reason([]) == ''
# --------------------------------------------------------------- blank fields
def _filled():
return {
'concrete': {'fc_psi': 6000, 'fci_psi': 3500, 'unit_weight_pcf': 150},
'thickness_in': 8,
'envelope': {'length': {'max_ft': 49.25}, 'width': {'max_ft': 10}},
'reinforcing': {
'wwf': {'mesh': '6x6 W2.9'},
'strand_groups': [{'qty': 14, 'dia_in': 0.5}],
'strand_layout': {'columns_in': [4, 16, 28]},
},
}
def test_a_full_spec_has_nothing_blank():
assert specsave.blank_fields(_filled()) == []
def test_a_missing_number_is_named_by_its_dotted_path():
spec = _filled()
spec['concrete']['fci_psi'] = None
assert specsave.blank_fields(spec) == ['concrete.fci_psi']
def test_an_empty_list_counts_as_blank():
spec = _filled()
spec['reinforcing']['strand_layout']['columns_in'] = []
assert specsave.blank_fields(spec) == \
['reinforcing.strand_layout.columns_in']
def test_an_empty_spec_names_every_field_rather_than_erroring():
assert len(specsave.blank_fields({})) == \
len(specsave.KEY_FIELDS) + len(specsave.LIST_FIELDS)
# ------------------------------------------------------------ what opens a fold
def test_nothing_wrong_means_no_problem_sentence():
assert specsave.problem_sentence([], []) is None
def test_unresolved_notes_open_the_section():
sentence = specsave.problem_sentence([('a', 'x')], [])
assert sentence and '1 note' in sentence
def test_validation_problems_open_the_section():
sentence = specsave.problem_sentence([], ["missing concrete.fc_psi"])
assert sentence and 'validation problem' in sentence
def test_blank_fields_alone_do_not_open_the_section():
# THE REGRESSION THIS GUARDS. The mechanical reader leaves the strand
# matrix empty on every spec it writes, so opening on a blank field would
# open every section on every press, and "if nothing is open, nothing
# needs you" would stop being true.
assert specsave.problem_sentence([], []) is None
def test_summary_line_says_both_numbers():
assert specsave.summary_line([('a', '')], ['x', 'y']) == \
'1 unresolved note, 2 fields blank'
# ------------------------------------------- nobody branches on the old value
#
# Specs on disk carry `source.extractor = 'claude-code'`, written before the
# owner's ruling named the model. The AI path stamps 'opus' from now on. Every
# reader has to tolerate both, and the way to keep that true is to have no
# reader that compares the field to a literal at all.
# Two shapes: a comparison on a line that names the field, and a comparison
# against one of the values only this field ever holds. 'regex' is NOT in the
# second list on purpose -- `ticketqc` puts the unrelated string 'regex' in
# `spec_source` (whether a spec FILE existed at all, not who wrote it), and a
# guard that fires on somebody else's field is a guard that gets deleted.
_EXT_COMPARE = re.compile(r"extractor[^\n]{0,60}[=!]=|"
r"[=!]=[^\n]{0,60}extractor|"
r"[=!]=\s*u?['\"](claude-code|claude|opus)['\"]")
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def test_no_code_branches_on_an_extractor_literal():
offenders = []
for folder, dirs, files in os.walk(_ROOT):
dirs[:] = [d for d in dirs if d not in ('__pycache__', 'tests')]
for name in files:
if not name.endswith('.py'):
continue
path = os.path.join(folder, name)
with io.open(path, 'r', encoding='utf-8') as fh:
for n, line in enumerate(fh, 1):
if line.lstrip().startswith('#'):
continue
if _EXT_COMPARE.search(line):
offenders.append('{0}:{1}'.format(path, n))
assert offenders == [], (
'these compare an extractor value to a literal; a spec written by an '
'older build says "claude-code" and would take the other branch: '
+ ', '.join(offenders))