Files
willtools-demo/WillTools.extension/tests/test_scrub.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
2026-09-03 16:33:47 -05:00

642 lines
28 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for tools/scrub.py -- the placeholder exemption on the Revit
model/family filename patterns, and the term-boundary fix that stops a
denylist term from hiding behind a glued-on underscore or digit.
`scrub.py` is host CPython (git subprocess calls, `from __future__ import
annotations`), so it is loaded the same way `tools/knowledge_drift.py` is in
`test_knowledge_drift.py`: put `tools/` on `sys.path` and import it directly,
rather than the by-path `importlib` trick `test_reinforceoffline.py` uses --
either works, this one is fewer lines for a module with no package-relative
imports of its own.
What is worth testing here is not "does scrub.py find things" in general --
it is specifically the shape of two exemptions/fixes going wrong:
* the placeholder exemption: the placeholder itself must pass, a genuine-
looking model filename must still fail, and -- the narrow way an
exemption like this goes too far -- a name that merely CONTAINS the
placeholder (`sample_job_...`) must still fail too.
* the term boundary: a denylist term glued to "_SOMETHING" or a digit must
now be caught, while a term that is a substring of an unrelated longer
word must still be let alone.
Both are checked at the unit level (`_is_placeholder_filename`,
`load_placeholders`, the compiled term pattern) and at the `scan_worktree` /
`scan_history` / `main` level, so a future refactor that keeps the unit
right but wires it up wrong still gets caught.
No real client, job, or plant name appears anywhere below. Every
"real-looking" filename and every term used to probe the boundary fix is
invented for this test and identifies no one -- and every one of them is
assembled at runtime rather than spelled whole in this file's own source
(the same trick `test_ticketqc_evidence.py` uses for its model path), because
`tools/scrub.py` correctly treats this file like any other: a near-miss or a
realistic-looking name is SUPPOSED to keep failing the gate, including when
`scrub.py` scans this file's own source, so writing one in whole would make
this test file permanently unable to pass `--worktree` / `--staged` itself.
"""
import os
import subprocess
import sys
import pytest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
TOOLS = os.path.join(ROOT, 'tools')
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
scrub = pytest.importorskip('scrub')
_RVT = '.' + 'rvt'
_RFA = '.' + 'rfa'
# A stand-in for CLAUDE.md, shaped exactly like the real Section 3 excerpt:
# the placeholder table plus the "Product codes" paragraph right after it,
# so a test against this fixture proves the same thing a test against the
# real file would, without depending on CLAUDE.md's prose staying byte-for-
# byte the same while five other agents may be editing it.
FAKE_CLAUDE_MD = """
## 3. Hard rules
**No client or job information in the repo. Ever.**
Not in code, not in comments, not in docstrings, not in a commit message, not
in an example. When you need to refer to one, write the placeholder rather
than deleting the sentence:
| | |
| --- | --- |
| a client / owner | `sample_client` |
| a job / project / model | `sample_job` |
Product codes are **not** client information and stay as they are: design
numbers (`PNL1.0`), piece marks (`PNL101`), hardware part numbers
(`ANCHOR_FACE_4T`) are naming conventions, not identities.
"""
PLACEHOLDERS = ['sample_client', 'sample_job']
# Invented, plausible-but-fake "real" model filenames -- what a genuine job
# might be named, but naming no actual client, job, or plant.
REAL_LOOKING_NAMES = [
'northgate_manufacturing_bldg_a' + _RVT,
'ridgeline_precast_ph2' + _RFA,
]
# Failure mode 1 from the ticket: the placeholder as a PREFIX/SUBSTRING of a
# longer name, not the whole name. These must never be treated as the
# placeholder itself.
NEAR_MISS_NAMES = [
'sample_job_real_client_name' + _RVT,
'sample_job_northgate' + _RFA,
'the_sample_job' + _RVT,
'sample_job2' + _RVT,
]
def _write(path, text):
with open(path, 'w', encoding='utf-8') as fh:
fh.write(text)
return str(path)
def _git(repo, *args):
subprocess.run(['git'] + list(args), cwd=str(repo), check=True,
capture_output=True, text=True)
@pytest.fixture
def git_repo(tmp_path):
"""An empty, isolated git repo -- never the real one -- for history
tests. `scan_history` shells out to git with `cwd=scrub.ROOT`, so
pointing `scrub.ROOT` here (see tests below) is enough to sandbox it."""
repo = tmp_path / 'repo'
repo.mkdir()
_git(repo, 'init', '-q')
# Assembled, not written whole, so this fixture email does not itself
# read as an email-shaped structural hit in this file's own source.
_git(repo, 'config', 'user.email', 'scrub-test' + '@' + 'example.invalid')
_git(repo, 'config', 'user.name', 'Test')
return repo
def _pats():
return scrub.patterns([])
# --------------------------------------------------------- load_placeholders
def test_load_placeholders_reads_the_real_source_of_truth():
"""The actual source of truth, read end to end: both documented
placeholders come back, and nothing else does.
No path is passed -- this exercises load_placeholders' own real
CLAUDE.md -> CONTRIBUTING.md fallback (see its docstring), not one
hardcoded file. In the owner's own checkout that reads CLAUDE.md; in a
knowledge-less distribution (tools/make_dist.py), CLAUDE.md does not
ship and this reads CONTRIBUTING.md instead, which carries the
identical heading and table verbatim -- so this test passes unchanged
in both trees, exactly the property it is meant to prove."""
names = scrub.load_placeholders()
assert 'sample_client' in names
assert 'sample_job' in names
def test_load_placeholders_from_table(tmp_path):
path = _write(tmp_path / 'CLAUDE.md', FAKE_CLAUDE_MD)
assert scrub.load_placeholders(path) == ['sample_client', 'sample_job']
def test_load_placeholders_excludes_product_code_examples(tmp_path):
"""`PNL1.0` / `PNL101` / `ANCHOR_FACE_4T` sit in prose, not a table row,
two paragraphs after the table -- and CLAUDE.md says outright they
'stay as they are'. They must never come back as an exemption."""
path = _write(tmp_path / 'CLAUDE.md', FAKE_CLAUDE_MD)
names = scrub.load_placeholders(path)
assert 'pnl1.0' not in names
assert 'pnl101' not in names
assert 'anchor_face_4t' not in names
def test_load_placeholders_missing_heading_is_empty(tmp_path):
path = _write(tmp_path / 'CLAUDE.md', '# unrelated file, no such rule\n')
assert scrub.load_placeholders(path) == []
def test_load_placeholders_missing_file_is_empty(tmp_path):
assert scrub.load_placeholders(str(tmp_path / 'does-not-exist.md')) == []
# ----------------------------------------------------- _is_placeholder_filename
@pytest.mark.parametrize('name', [
'sample_job' + _RVT, 'sample_job' + _RFA,
'sample_client' + _RVT, 'sample_client' + _RFA,
'SAMPLE_JOB' + _RVT.upper(), 'Sample_Job' + _RVT.title(),
])
def test_placeholder_filename_is_exempt(name):
assert scrub._is_placeholder_filename(name, PLACEHOLDERS) is True
@pytest.mark.parametrize('name', NEAR_MISS_NAMES + REAL_LOOKING_NAMES)
def test_placeholder_filename_is_not_exempt(name):
# Failure mode 1 lives in NEAR_MISS_NAMES: the placeholder as a
# substring of a longer name, not the whole name -- the exact shape
# that must keep failing.
assert scrub._is_placeholder_filename(name, PLACEHOLDERS) is False
def test_no_placeholders_loaded_exempts_nothing():
"""If CLAUDE.md can't be read, the gate must fail SAFE -- stricter, not
looser -- so the placeholder itself is still flagged."""
assert scrub._is_placeholder_filename('sample_job' + _RVT, []) is False
assert scrub._is_placeholder_filename('sample_job' + _RVT, None) is False
# ------------------------------------------------------------- scan_worktree
def test_scan_worktree_placeholder_passes(tmp_path, monkeypatch):
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'fixture.py',
"MODEL_PATH = '{0}'\n".format('sample_job' + _RVT))
hits = scrub.scan_worktree(_pats(), ['fixture.py'], PLACEHOLDERS)
assert hits == []
@pytest.mark.parametrize('name', REAL_LOOKING_NAMES)
def test_scan_worktree_real_looking_name_still_fails(tmp_path, monkeypatch, name):
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'fixture.py', "MODEL_PATH = '{0}'\n".format(name))
hits = scrub.scan_worktree(_pats(), ['fixture.py'], PLACEHOLDERS)
assert len(hits) == 1
assert 'filename' in hits[0]
def test_scan_worktree_near_miss_still_fails(tmp_path, monkeypatch):
"""The too-broad failure mode from the ticket, run through the real
scanning path: a placeholder-prefixed name that is not the placeholder
must still be a hit."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'fixture.py',
"MODEL_PATH = '{0}'\n".format(NEAR_MISS_NAMES[0]))
hits = scrub.scan_worktree(_pats(), ['fixture.py'], PLACEHOLDERS)
assert len(hits) == 1
def test_scan_worktree_untracked_placeholder_passes(tmp_path, monkeypatch):
"""The exemption applies the same way whether the caller is scanning a
tracked list or the untracked one -- `scan_worktree` does not know or
care which; both `--worktree` and `--staged`'s loose-file pass go
through it."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'scratch.py',
"m = '{0}'\n".format('sample_job' + _RVT))
hits = scrub.scan_worktree(_pats(), ['scratch.py'], PLACEHOLDERS)
assert hits == []
# -------------------------------------------------------------- scan_history
def test_scan_history_placeholder_passes(git_repo, monkeypatch):
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
_write(git_repo / 'fixture.py',
"MODEL_PATH = '{0}'\n".format('sample_job' + _RVT))
_git(git_repo, 'add', 'fixture.py')
_git(git_repo, 'commit', '-q', '-m', 'add fixture')
hits = scrub.scan_history(_pats(), PLACEHOLDERS)
assert hits == []
def test_scan_history_real_looking_name_still_fails(git_repo, monkeypatch):
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
_write(git_repo / 'fixture.py',
"MODEL_PATH = '{0}'\n".format(REAL_LOOKING_NAMES[0]))
_git(git_repo, 'add', 'fixture.py')
_git(git_repo, 'commit', '-q', '-m', 'add fixture')
hits = scrub.scan_history(_pats(), PLACEHOLDERS)
assert len(hits) == 1
def test_scan_history_near_miss_still_fails(git_repo, monkeypatch):
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
_write(git_repo / 'fixture.py',
"MODEL_PATH = '{0}'\n".format(NEAR_MISS_NAMES[0]))
_git(git_repo, 'add', 'fixture.py')
_git(git_repo, 'commit', '-q', '-m', 'add fixture')
hits = scrub.scan_history(_pats(), PLACEHOLDERS)
assert len(hits) == 1
def test_scan_history_no_placeholders_fails_safe(git_repo, monkeypatch):
"""The same fail-safe direction as `_is_placeholder_filename`: if
`placeholders` is empty, history scanning flags the placeholder name
too rather than silently exempting it on an unproven assumption."""
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
_write(git_repo / 'fixture.py',
"MODEL_PATH = '{0}'\n".format('sample_job' + _RVT))
_git(git_repo, 'add', 'fixture.py')
_git(git_repo, 'commit', '-q', '-m', 'add fixture')
hits = scrub.scan_history(_pats(), [])
assert len(hits) == 1
# ------------------------------------------------------- term boundary (\b hole)
# A fake stand-in for the real leak found in tracked files today: `\b` does
# not sit between a letter and an underscore (both are "word" characters to
# `re`), so `\bTerm\b` never matched a term glued to a role identifier or a
# piece mark, e.g. TERM_EMBED_P76. "Northgate" here is invented for this
# test and is not a real client, job, or plant name.
FAKE_TERM = 'Northgate'
def test_term_pattern_catches_underscore_glued_hit():
pat, why = scrub.patterns([(FAKE_TERM, False)])[-1]
assert pat.search('ref = ' + FAKE_TERM.upper() + '_EMBED_P76') is not None
def test_term_pattern_catches_digit_glued_hit():
pat, why = scrub.patterns([(FAKE_TERM, False)])[-1]
assert pat.search('model = ' + FAKE_TERM + '2026') is not None
def test_term_pattern_still_excludes_longer_word():
"""The other half of the same rule: a term that is a substring of an
ordinary longer word -- letters on both sides -- must not fire, or
every term becomes a landmine for unrelated vocabulary."""
pat, why = scrub.patterns([(FAKE_TERM, False)])[-1]
assert pat.search(FAKE_TERM + 'way Logistics Inc') is None
assert pat.search('pre' + FAKE_TERM + 'Setup') is None
def test_term_pattern_still_matches_plain_word_boundary():
"""The ordinary case `\\b` already handled -- whitespace either side --
must keep working; this is a widening of the boundary, not a swap to
something unrelated."""
pat, why = scrub.patterns([(FAKE_TERM, False)])[-1]
assert pat.search('the ' + FAKE_TERM + ' project') is not None
def test_scan_worktree_catches_underscore_glued_term(tmp_path, monkeypatch):
"""The hole, exercised through the real scanning path a commit would
actually go through, not just the compiled pattern in isolation."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'fixture.py',
"REF = '{0}'\n".format(FAKE_TERM.upper() + '_EMBED_P76'))
pats = scrub.patterns([(FAKE_TERM, False)])
hits = scrub.scan_worktree(pats, ['fixture.py'], [])
assert len(hits) == 1
assert FAKE_TERM.lower() in hits[0].lower()
# --------------------------------------------------------------------- main
def test_main_worktree_end_to_end(git_repo, monkeypatch):
"""The whole CLI path, `--worktree`, wired together: placeholder in a
tracked file is clean; a real-looking name added afterwards (untracked,
since `--worktree` must catch those too) fails."""
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
monkeypatch.setattr(scrub, 'CLAUDE_MD', str(git_repo / 'CLAUDE.md'))
_write(git_repo / 'CLAUDE.md', FAKE_CLAUDE_MD)
terms_path = _write(git_repo / 'empty-terms.txt', '')
_write(git_repo / 'fixture.py',
"MODEL_PATH = '{0}'\n".format('sample_job' + _RVT))
_git(git_repo, 'add', '-A')
monkeypatch.setattr(sys, 'argv',
['scrub.py', '--worktree', '--terms', terms_path])
assert scrub.main() == 0
_write(git_repo / 'other.py',
"MODEL_PATH = '{0}'\n".format(REAL_LOOKING_NAMES[0]))
assert scrub.main() == 1
# ------------------------------------------------------ --private / the gitignored tree
# `QNA/`, `knowledge/plant/` and `knowledge/job/` are gitignored, so every
# mode above is blind to them by construction. `--private` is the mode that
# looks, and what is worth testing about it is not "does it find names" --
# in that tree it finds thousands, and that is correct -- but the two things
# that keep it from being a wall of expected hits nobody reads:
#
# * the resident/travelling split. A name in a sentence is the file doing
# its job. The same name inside a fence, a quoted literal, a proposed
# `key: value`, a path, or glued into a longer identifier is a string
# shaped to be copied into the public repo, and that is what gets listed.
# * aggregation by the distinct string, so one identifier mentioned four
# hundred times is one row.
#
# And one thing that must NOT have changed: `--staged` still blocks on staged
# files only. A gate that starts refusing commits over a note in the private
# repo is a gate that gets passed with --no-verify, and then it guards
# nothing -- so there is a test below pinning that, right next to the new
# code that could plausibly have broken it.
_FAKE_PLANT = 'Ridgeline' # invented; identifies no real plant
def _plant_pats():
"""Just the fake plant term, built through `patterns()` so these tests
exercise the real boundary logic; the structural patterns are dropped so
a fixture cannot accidentally pass by matching one of those instead."""
return scrub.patterns([(_FAKE_PLANT, False)])[len(scrub.STRUCTURAL):]
def _travels(line, kind='doc', why="term 'x'", in_fence=False):
"""`_travels` for the first occurrence of the fake plant name on `line`."""
return scrub._travels(kind, line, line.lower().index(_FAKE_PLANT.lower()),
why, in_fence)
# ------------------------------------------------------------- private_files
def test_private_files_drops_dot_segments(tmp_path, monkeypatch):
"""A browser profile cache under QNA/ is 58,000 files on its own. Dot
names in a working tree are tool state, not documents, and reading them
would bury the report they are supposed to be part of."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'note.md', 'x\n')
keep, dropped = scrub.private_files(
['note.md', '.cache/a.md', 'QNA/.agent-home/profiles/b.md'])
assert keep == ['note.md']
assert dropped['tool state'] == 2
def test_private_files_drops_build_output(tmp_path, monkeypatch):
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'note.md', 'x\n')
keep, dropped = scrub.private_files(
['note.md', 'lib/__pycache__/a.py', 'dist/b.py'])
assert keep == ['note.md']
assert dropped['build output'] == 2
def test_private_files_drops_unreadable_and_counts_them(tmp_path, monkeypatch):
"""A ticket PDF carries the job name in its title block and nothing here
can read it. It has to be COUNTED, not passed over: "not scanned" and
"scanned, clean" are different answers and a scrub report may not
collapse them."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'note.md', 'x\n')
keep, dropped = scrub.private_files(
['note.md', 'tickets/a.pdf', 'img/b.png'])
assert keep == ['note.md']
assert dropped['unreadable'] == 2
def test_private_files_drops_over_size_cap(tmp_path, monkeypatch):
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
monkeypatch.setattr(scrub, 'PRIVATE_MAX_BYTES', 10)
_write(tmp_path / 'small.md', 'x\n')
_write(tmp_path / 'big.md', 'x' * 50)
keep, dropped = scrub.private_files(['small.md', 'big.md'])
assert keep == ['small.md']
assert dropped['over size cap'] == 1
def test_private_files_accounts_for_every_input(tmp_path, monkeypatch):
"""Kept plus dropped equals what went in. Nothing may fall out of the
walk unnoticed -- the count in the report is a coverage claim."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'note.md', 'x\n')
listed = ['note.md', '.cache/a.md', 'dist/b.py', 'c.pdf', 'gone.md']
keep, dropped = scrub.private_files(listed)
assert len(keep) + sum(dropped.values()) == len(listed)
assert dropped['gone'] == 1
def test_ignored_files_returns_what_git_is_ignoring(git_repo, monkeypatch):
"""The git plumbing itself: `--others --ignored` is the mirror image of
the `--others --exclude-standard` walk the untracked scan uses."""
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
_write(git_repo / '.gitignore', 'private/\n')
(git_repo / 'private').mkdir()
_write(git_repo / 'private' / 'note.md', 'x\n')
_write(git_repo / 'open.md', 'x\n')
found = [f.replace('\\', '/') for f in scrub.ignored_files()]
assert 'private/note.md' in found
assert 'open.md' not in found
# ------------------------------------------------------------------ _travels
def test_prose_mention_in_a_document_is_resident():
"""The whole point. A producer's name in a sentence about that producer,
in the repository that documents them, is not a finding."""
assert _travels('The ' + _FAKE_PLANT + ' manual calls this a soffit.') is None
@pytest.mark.parametrize('line, in_fence, expected', [
(' key = "' + _FAKE_PLANT + '"', True, 'in a fenced code block'),
('set `' + _FAKE_PLANT + '_THK` on the piece', False, 'in inline code'),
('the sheet says "' + _FAKE_PLANT + '" in the corner', False,
'in a quoted literal'),
('plant_id: ' + _FAKE_PLANT.lower(), False, 'in a key: value line'),
('see knowledge/plant/' + _FAKE_PLANT.lower() + '/knowledge.yaml', False,
'in a path'),
])
def test_travelling_shapes_in_a_document(line, in_fence, expected):
assert _travels(line, in_fence=in_fence) == expected
def test_prose_colon_line_is_not_mistaken_for_a_yaml_shape():
"""`Note: the plant says X` is a sentence, not a proposed key. The key
pattern requires a lowercase, space-free key, which is what keeps an
ordinary English colon out of the travelling pile."""
assert _travels('Note: the ' + _FAKE_PLANT + ' manual disagrees') is None
assert _travels('the plant says: ' + _FAKE_PLANT + ' does it this way') is None
def test_data_file_value_is_resident_but_a_comment_travels():
"""Values in a plant or job file ARE the file. A comment is prose written
for a reader, and it is what gets carried across when the shape of the
file is proposed somewhere else."""
assert _travels(' plant: ' + _FAKE_PLANT.lower(), kind='data') is None
assert _travels(' thk: 4 # ' + _FAKE_PLANT + ' always uses 4',
kind='data') == 'in a comment'
def test_code_file_always_travels():
"""A private script is all instruction; there is no prose half of it for
a name to be resident in."""
assert _travels('# a note about ' + _FAKE_PLANT, kind='code') == 'in code'
def test_structural_always_travels_whatever_the_file_is():
"""A user path, an address or a model filename names one person, one
machine or one job. There is no 'doing its job' reading of it to weigh
against travelling, in any kind of file."""
for kind in ('doc', 'data', 'code'):
assert scrub._travels(kind, 'path = x', 0,
'absolute Windows user path', False) \
== 'structural'
# -------------------------------------------------------------- scan_private
def test_scan_private_aggregates_one_string_across_many_places(tmp_path,
monkeypatch):
"""One identifier mentioned in three files is ONE row with a count, not
three lines. This is what keeps the report short enough to be read."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
ident = _FAKE_PLANT.upper() + '_EMBED_P76'
for name in ('a.md', 'b.md', 'c.md'):
_write(tmp_path / name, 'use `' + ident + '` here\n')
travelling, _, _ = scrub.scan_private(
_plant_pats(), ['a.md', 'b.md', 'c.md'], [])
assert list(travelling) == [ident]
assert travelling[ident][0] == 3
def test_scan_private_marks_a_name_glued_into_an_identifier(tmp_path,
monkeypatch):
"""The `glued` flag, which sorts these to the top: a name buried inside a
longer token is the one a human reader skims past. A bare backticked
mention is still a word somebody can see is a name."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
ident = _FAKE_PLANT.upper() + '_EMBED_P76'
_write(tmp_path / 'glued.md', 'use `' + ident + '` here\n')
_write(tmp_path / 'bare.md', 'use `' + _FAKE_PLANT + '` here\n')
travelling, _, _ = scrub.scan_private(
_plant_pats(), ['glued.md', 'bare.md'], [])
assert travelling[ident][3] is True
assert travelling[_FAKE_PLANT][3] is False
def test_scan_private_reports_the_files_own_name_separately(tmp_path,
monkeypatch):
"""A folder named after the job puts the name in the path of every file
under it. Those are aggregated into their own pile and counted -- 380
identical lines would drown everything else -- but they are reported,
because a tracked file that quotes one of those paths publishes it."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
sub = tmp_path / (_FAKE_PLANT.lower() + '_job')
sub.mkdir()
for name in ('one.md', 'two.md'):
_write(sub / name, 'nothing interesting\n')
listed = [_FAKE_PLANT.lower() + '_job/one.md',
_FAKE_PLANT.lower() + '_job/two.md']
travelling, named_dirs, resident = scrub.scan_private(
_plant_pats(), listed, [])
assert travelling == {}
assert resident == {}
assert list(named_dirs.values())[0][0] == 2
def test_scan_private_counts_prose_without_listing_it(tmp_path, monkeypatch):
"""Resident matches come back as a number per file and never as a line.
A wall of expected hits is a report nobody reads twice."""
monkeypatch.setattr(scrub, 'ROOT', str(tmp_path))
_write(tmp_path / 'note.md',
('The ' + _FAKE_PLANT + ' manual says so.\n') * 5)
travelling, named_dirs, resident = scrub.scan_private(
_plant_pats(), ['note.md'], [])
assert travelling == {}
assert named_dirs == {}
assert resident == {'note.md': 5}
# --------------------------------------------------------------- main --private
def test_main_private_exits_zero_even_when_it_finds_things(git_repo,
monkeypatch, capsys):
"""--private is an inventory, not a gate. Every string it prints carries
a denylist term or a structural pattern, so a copy of one into a tracked
file is already a --worktree failure -- there is no leak here for an exit
code to catch that is not caught one directory out. A mode that returned
non-zero on the raw material would return non-zero every run, and a check
that always fails gets wrapped in `|| true`."""
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
monkeypatch.setattr(scrub, 'CLAUDE_MD', str(git_repo / 'CLAUDE.md'))
_write(git_repo / 'CLAUDE.md', FAKE_CLAUDE_MD)
# Outside the repo on purpose: a terms file that holds the term, staged,
# would be a hit on itself and would prove nothing about anything else.
terms = _write(git_repo.parent / 'terms.txt', _FAKE_PLANT + '\n')
_write(git_repo / '.gitignore', 'private/\n')
(git_repo / 'private').mkdir()
_write(git_repo / 'private' / 'note.md',
'use `' + _FAKE_PLANT.upper() + '_EMBED_P76` here\n')
_git(git_repo, 'add', '-A')
_git(git_repo, 'commit', '-q', '-m', 'init')
monkeypatch.setattr(sys, 'argv',
['scrub.py', '--private', '--terms', terms])
assert scrub.main() == 0
out = capsys.readouterr().out
assert 'TRAVELLING' in out
assert _FAKE_PLANT.upper() + '_EMBED_P76' in out
def test_private_mode_did_not_change_what_staged_blocks_on(git_repo,
monkeypatch):
"""The rule --private must not have disturbed: the hook blocks on STAGED
files and nothing else. A name in the private tree is loud in its own
mode and silent here, because a gate that refuses a commit over a note in
the other repository is a gate that gets passed with --no-verify."""
monkeypatch.setattr(scrub, 'ROOT', str(git_repo))
monkeypatch.setattr(scrub, 'CLAUDE_MD', str(git_repo / 'CLAUDE.md'))
_write(git_repo / 'CLAUDE.md', FAKE_CLAUDE_MD)
# Outside the repo on purpose: a terms file that holds the term, staged,
# would be a hit on itself and would prove nothing about anything else.
terms = _write(git_repo.parent / 'terms.txt', _FAKE_PLANT + '\n')
_write(git_repo / '.gitignore', 'private/\n')
(git_repo / 'private').mkdir()
_write(git_repo / 'private' / 'note.md',
'use `' + _FAKE_PLANT.upper() + '_EMBED_P76` here\n')
_write(git_repo / 'clean.py', 'x = 1\n')
_git(git_repo, 'add', 'clean.py', '.gitignore', 'CLAUDE.md')
monkeypatch.setattr(sys, 'argv',
['scrub.py', '--staged', '--terms', terms])
assert scrub.main() == 0
_write(git_repo / 'dirty.py', "plant = '{0}'\n".format(_FAKE_PLANT))
_git(git_repo, 'add', 'dirty.py')
assert scrub.main() == 1