52d94303bc
ci / test (push) Has been cancelled
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.
642 lines
28 KiB
Python
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
|