# -*- coding: utf-8 -*- """The offline CLI: `tools/reinforce_offline.py`, run with no Revit at all. WHAT THESE TESTS ARE REALLY FOR -------------------------------- The claim this feature rests on is a claim about an IMPORT GRAPH: that the whole chain from an index file to a printed suggestion touches nothing in `Autodesk.*`. That claim cannot be argued, only run -- and running it here is the proof, because host CPython has no Revit API at all, so a stray module-scope `from Autodesk.Revit.DB import ...` anywhere on the path does not degrade, it raises ImportError and this file goes red. The rest is the wording an offline reader depends on: that a snapshot is never presented as a model, that a missing index says what to do about it, and that an index from another schema is refused whole rather than half-read. Every mark here is an invented `PNL...` placeholder, as everywhere else in this suite. No real mark, job or client name appears. """ # pylint: disable=import-error,invalid-name import importlib.util import json import os import sys from pathlib import Path import pytest from vibe import foldreport as fold from vibe import reinforcecore as core CLI_PATH = Path(__file__).resolve().parents[2] / 'tools' / 'reinforce_offline.py' def _load_cli(): spec = importlib.util.spec_from_file_location('reinforce_offline', str(CLI_PATH)) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module cli = _load_cli() # ------------------------------------------------------------------ fixtures def _piece(name, rects, reinforced=1, on_hold=0, master=False): feats = [core.feature(name, i, r[0], r[1], r[2], r[3], True, 0.0, 30.0, 10.0, 8.0) for i, r in enumerate(rects)] return core.piece_record(name, 100 + len(name), 'sig', reinforced, on_hold, design_number='PNL1.0', length_ft=30.0, width_ft=10.0, thickness_in=8.0, features=feats, master=master) @pytest.fixture def snapshot(tmp_path): """A jobs root with one model in it, holding a two-donor index.""" folder = tmp_path / 'jobs' / 'sample_job' / 'reinforce' folder.mkdir(parents=True) cache = core.new_cache('sample_job') cache['built_at'] = '2026-08-30 09:00:00' cache['scope'] = {'prefixes': ['PNL'], 'note': None, 'declared_by': None} pieces = [ _piece('PNL800', [(4.0, 6.0, 2.0, 5.0), (20.0, 22.0, 2.0, 5.0)], reinforced=0), # the target: not a donor _piece('PNL801', [(4.0, 6.0, 2.0, 5.0), (20.0, 22.0, 2.0, 5.0)]), _piece('BM802', [(4.0, 6.0, 2.0, 5.0)]), # another engineer's piece ] cache['pieces'] = dict((p['name'], p) for p in pieces) core.save(str(folder / 'index.json'), cache) return tmp_path / 'jobs' # ----------------------------------------------------- the import graph claim def test_no_part_of_the_offline_path_touches_revit(snapshot, capsys): """The claim, run rather than argued. See the module docstring.""" before = set(sys.modules) assert cli.main(['PNL800', '--jobs', str(snapshot), '--model', 'sample_job']) == 0 capsys.readouterr() loaded = set(sys.modules) - before forbidden = [m for m in set(sys.modules) if m.split('.')[0] in ('Autodesk', 'clr', 'pyrevit', 'System', 'Microsoft')] assert forbidden == [], forbidden # And the modules that DID load are the ones that were promised to be free # of it, which is the other half of the claim. assert 'vibe.reinforcebind' in loaded or 'vibe.reinforcebind' in sys.modules def test_the_two_functions_that_need_a_document_are_named_and_not_called(): """`outline_audit` and `suggest_for_piece` are the Revit-bound pair. They are importable -- the module has no Revit at module scope -- and calling either is what an offline caller must not do.""" from vibe import reinforcebind as bind assert callable(bind.outline_audit) assert callable(bind.suggest_for_piece) assert callable(bind.suggest_from_index) # the one this path uses assert callable(bind.load_audit_at) # the doc-free audit reader # -------------------------------------------------------------- finding it def test_an_explicit_path_wins_and_is_named_in_the_output(snapshot): path, how = cli.resolve_index( str(snapshot / 'sample_job' / 'reinforce' / 'index.json')) assert path.endswith('index.json') assert '--index' in how def test_a_jobs_root_plus_a_model_resolves_the_standard_layout(snapshot): path, how = cli.resolve_index(None, str(snapshot), 'sample_job') assert os.path.isfile(path) assert 'sample_job' in how def test_one_model_under_the_root_needs_no_naming(snapshot): path, _how = cli.resolve_index(None, str(snapshot), None) assert os.path.isfile(path) def test_several_models_refuse_to_be_guessed_between(snapshot): (snapshot / 'another_job').mkdir() with pytest.raises(SystemExit) as got: cli.resolve_index(None, str(snapshot), None) assert 'Which model' in str(got.value) def test_no_index_and_no_jobs_root_says_all_three_ways_to_give_one(monkeypatch): monkeypatch.delenv('WILLTOOLS_JOBS', raising=False) with pytest.raises(SystemExit) as got: cli.resolve_index(None, None, None) message = str(got.value) assert '--index' in message and '--jobs' in message and 'WILLTOOLS_JOBS' \ in message def test_a_missing_index_says_it_is_written_from_inside_revit(tmp_path): with pytest.raises(SystemExit) as got: cli.load_index(str(tmp_path / 'nope.json')) assert 'from inside Revit' in str(got.value) def test_an_index_from_another_schema_is_refused_whole(tmp_path): """Never half-read: a donors-only file would answer 'not in the index' for every ordinary target while looking entirely healthy.""" path = tmp_path / 'index.json' path.write_text(json.dumps({'schema': core.SCHEMA - 1, 'pieces': {}}), encoding='utf-8') with pytest.raises(SystemExit) as got: cli.load_index(str(path)) message = str(got.value) assert 'discarded whole' in message assert 'Reinforcing Suggestion' in message # and what to do about it # ------------------------------------------------------------------ the run def test_a_target_that_may_not_donate_is_answered_from_the_snapshot(snapshot): """The whole point: PNL800 is not reinforced, so before schema 5 it was not in the file at all and only a live measurement could answer it.""" folder = str(snapshot / 'sample_job' / 'reinforce') cache = cli.load_index(os.path.join(folder, 'index.json')) suggestion, info, sections = cli.run(cache, 'PNL800', folder) assert suggestion is not None assert info['offline'] is True assert info['donor_names'] == ['PNL801'] # BM802 is out of scope text = fold.text(sections, show_all=True) assert 'OFFLINE' in text assert '2026-08-30 09:00:00' in text assert 'checked against the model' in text def test_the_stamped_scope_is_used_and_can_be_overridden(snapshot): folder = str(snapshot / 'sample_job' / 'reinforce') cache = cli.load_index(os.path.join(folder, 'index.json')) _s, bounded, _sections = cli.run(cache, 'PNL800', folder) assert bounded['rejected'].get('prefix') == 1 _s, unbounded, _sections = cli.run(cache, 'PNL800', folder, no_scope=True) assert 'prefix' not in unbounded['rejected'] def test_an_index_with_no_stamped_scope_says_so_rather_than_assuming(snapshot): """'Nobody recorded it' is not 'no scope was declared'.""" folder = str(snapshot / 'sample_job' / 'reinforce') path = os.path.join(folder, 'index.json') cache = cli.load_index(path) cache['scope'] = None _s, info, sections = cli.run(cache, 'PNL800', folder) assert info['scope_stamped'] is False # It goes in the HEADLINE, not a folded section: every count below rests # on a pool bounded by nobody knows what. assert any('no declared scope' in b[1] for b in sections[0].blocks if b[0] == 'md') def test_listing_keeps_donors_and_targets_apart(snapshot): cache = cli.load_index(str(snapshot / 'sample_job' / 'reinforce' / 'index.json')) donors, targets = cli.listing(cache) assert donors == ['BM802', 'PNL801'] assert targets == ['PNL800'] # ------------------------------------------------------------- the printing def test_entities_become_ascii_and_the_words_are_left_alone(): got = cli.plain('This is **bold** — and `code`.') assert '--' in got assert '—' not in got assert 'This is bold -- and code.' in got def test_a_quote_block_is_marked_so_a_caveat_cannot_look_like_prose(): assert cli.plain('> a warning').lstrip().startswith('!') def test_a_table_never_truncates_a_mark_however_wide_the_row_gets(): """A silently cut mark is a wrong answer that looks like a right one.""" got = cli.table(['donor', 'what'], [['PNL803', 'x' * 120], ['PNL804', 'y']]) assert 'PNL803' in got and 'PNL804' in got assert 'x' * 120 in got def test_an_empty_table_says_nothing_rather_than_printing_a_header(): assert 'nothing' in cli.table(['a'], []) # ------------------------------------------------------- what folds at a shell def test_the_terminal_folds_the_routine_sections_to_one_line(snapshot, capsys): """The owner's rule, at a command line: the answer, then a short list of what else there is. `--all` opens everything.""" args = ['PNL800', '--jobs', str(snapshot), '--model', 'sample_job'] cli.main(args) folded = _flat(capsys.readouterr().out) cli.main(args + ['--all']) everything = _flat(capsys.readouterr().out) assert len(everything) > len(folded) assert '(folded)' in folded assert 'How to read the numbers' in folded # named, though folded assert 'finest tolerance' not in folded # but not printed in full assert 'finest tolerance' in everything def test_the_unfolded_report_can_always_be_written_out(snapshot, tmp_path, capsys): """Folded is not gone. A fold that will not open must cost a file open, never a lost sentence -- so the file holds every section in full.""" out = tmp_path / 'full.txt' cli.main(['PNL800', '--jobs', str(snapshot), '--model', 'sample_job', '--out', str(out)]) printed = _flat(capsys.readouterr().out) written = _flat(out.read_text(encoding='utf-8')) assert 'finest tolerance' in written assert 'finest tolerance' not in printed assert str(out) in written # the page names itself def _flat(text): """Whitespace-normalised. The plain-text renderer wraps at 78 columns, so a phrase these tests look for can be split across two lines.""" return ' '.join(text.split())