# -*- coding: utf-8 -*- """`tools/sweep.py` refuses job files measured from a different model. THE INCIDENT, 2026-09-01 ------------------------ A whole sweep -- 184 job files -- carried `source.model` naming one model, while the document open in Revit was a detached copy that had since DIVERGED from it: a notch measured 3.5 in in one and 2.5 in in the other. Nothing looked wrong. Every report rendered, every weight reconciled, and it reconciled because a job file is the complete input to the check: the numbers agree with the file they came from whether or not that file is about the piece in front of you. The two were told apart by hand, and the field that would have answered it in one line was sitting in every job file unread. So the guard is a REFUSAL with both model names in it, not a warning line, and "could not ask Revit" is a third answer that is printed and never mistaken for agreement. `tools/sweep.py` is plain host CPython -- it drives Revit through `rvt.py` as a subprocess and imports nothing from the API -- so unlike `sweep_in_revit.py` it can simply be loaded by path and called. """ import importlib.util import sys import pytest from pathlib import Path REPO = Path(__file__).resolve().parents[2] SWEEP_PY = REPO / 'tools' / 'sweep.py' def _load(): spec = importlib.util.spec_from_file_location('_sweep_under_test', str(SWEEP_PY)) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module sweep = _load() def job(model, mark='PNL101'): return (mark, {'panel': {'mark': mark}, 'source': ({} if model is None else {'model': model})}) # ----------------------------------------------------------- the comparison @pytest.mark.parametrize('name,stem', [ ('sample_job.rvt', 'sample_job'), ('sample_job', 'sample_job'), ('SAMPLE_JOB.RVT', 'sample_job'), (r'C:\models\sample_job.rvt', 'sample_job'), (' sample_job.rvt ', 'sample_job'), ]) def test_the_extension_and_the_path_are_not_part_of_the_name(name, stem): """`source.model` is a basename with `.rvt` on it and `doc.Title` is not, so a literal comparison would call every job file a mismatch.""" assert sweep.model_stem(name) == stem @pytest.mark.parametrize('empty', [None, '', ' ', '.rvt']) def test_a_nameless_model_is_none_and_never_a_match(empty): assert sweep.model_stem(empty) is None # ------------------------------------------------------------- the mismatch def test_a_job_from_another_model_is_a_mismatch(): mismatched, unstamped, reason = sweep.check_source_model( [job('sample_job.rvt')], 'sample_job_detached') assert reason is None and unstamped == [] assert mismatched == [('PNL101', 'sample_job.rvt')] def test_the_same_model_is_not_a_mismatch(): mismatched, unstamped, reason = sweep.check_source_model( [job('sample_job.rvt')], 'sample_job') assert (mismatched, unstamped, reason) == ([], [], None) def test_the_refusal_names_both_models(): """The one thing the message must do. A refusal that says 'wrong model' without saying which two leaves the reader exactly where they were.""" mismatched, _unstamped, _reason = sweep.check_source_model( [job('sample_job.rvt'), job('sample_job.rvt', 'PNL102')], 'sample_job_detached') message = sweep.model_refusal(mismatched, 'sample_job_detached', 2) assert 'sample_job.rvt' in message assert 'sample_job_detached' in message assert '2 of 2' in message assert 'PNL101' in message assert 'Nothing was checked' in message assert '--any-model' in message def test_a_job_with_no_source_model_is_unjudged_not_cleared(): """"Written before the field existed" is not "measured from this model". It comes back in its own list so the caller can say so.""" mismatched, unstamped, reason = sweep.check_source_model( [job(None)], 'sample_job') assert mismatched == [] and reason is None assert unstamped == ['PNL101'] def test_no_open_document_is_could_not_judge_and_says_why(): """The empty `mismatched` here means DID NOT ASK. Reading it as agreement is the whole failure this guard exists for, one level up.""" mismatched, unstamped, reason = sweep.check_source_model( [job('sample_job.rvt')], None) assert mismatched == [] assert unstamped == ['PNL101'] assert reason and 'no open document' in reason # ------------------------------------------------ the refusal actually fires class _Recorder(object): """A sweep folder on disk is a lot of scaffolding for one question, so `check()` is exercised against a jobs dir and stopped before it ever reaches panellift -- the refusal is meant to happen first.""" def __init__(self): self.ran = 0 def _sweep_folder(tmp_path, model): jobs = tmp_path / 'jobs' jobs.mkdir() (tmp_path / 'meta').mkdir() (jobs / 'PNL101.job.json').write_text( '{"panel": {"mark": "PNL101"}, "source": {"model": "%s"}}' % model, encoding='utf-8') (tmp_path / 'meta' / 'PNL101.json').write_text( '{"mark": "PNL101", "status": "exported"}', encoding='utf-8') return tmp_path def test_check_refuses_before_it_computes_anything(tmp_path, monkeypatch): root = _sweep_folder(tmp_path, 'sample_job.rvt') seen = _Recorder() def explode(*args, **kwargs): seen.ran += 1 raise AssertionError('panellift must not run on a mismatched sweep') monkeypatch.setattr(sweep.subprocess, 'run', explode) with pytest.raises(SystemExit) as excinfo: sweep.check(str(root), 'sample_job_detached') assert 'sample_job.rvt' in str(excinfo.value) assert 'sample_job_detached' in str(excinfo.value) assert seen.ran == 0 def test_any_model_proceeds_and_carries_the_caveat_into_the_report(tmp_path, monkeypatch): """The override is not a way to make the sentence go away: it goes to the top of problems.txt, where `render` puts it.""" root = _sweep_folder(tmp_path, 'sample_job.rvt') class _Result(object): stdout = '{"verdict": "OK", "stages": []}' stderr = '' monkeypatch.setattr(sweep.subprocess, 'run', lambda *a, **k: _Result()) rows, notes = sweep.check(str(root), 'sample_job_detached', any_model=True) assert len(rows) == 1 assert any('--any-model' in note for note in notes) assert notes[0].startswith('!!') assert notes[0] in sweep.render(rows, [], notes) def test_an_unreachable_revit_says_the_model_was_not_verified(tmp_path, monkeypatch): """It does not refuse -- there is nothing to refuse against -- and it does not stay quiet either.""" root = _sweep_folder(tmp_path, 'sample_job.rvt') class _Result(object): stdout = '{"verdict": "OK", "stages": []}' stderr = '' monkeypatch.setattr(sweep.subprocess, 'run', lambda *a, **k: _Result()) _rows, notes = sweep.check(str(root), None) assert any('NOT verified' in note for note in notes) def test_a_matching_model_adds_no_caveat_at_all(tmp_path, monkeypatch): """The ordinary run is unchanged, down to the absence of a banner.""" root = _sweep_folder(tmp_path, 'sample_job.rvt') class _Result(object): stdout = '{"verdict": "OK", "stages": []}' stderr = '' monkeypatch.setattr(sweep.subprocess, 'run', lambda *a, **k: _Result()) _rows, notes = sweep.check(str(root), 'sample_job') assert notes == []