test(m1): seed anonymised fixture corpus with hygiene scan

This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 19:36:06 +02:00
commit b538b5c5f0
17 changed files with 910 additions and 0 deletions

View file

@ -0,0 +1,189 @@
"""The fixture corpus is anonymised, and this test is what makes that a fact
rather than an intention (plan Step 7).
This repository has a PUBLIC remote. A fixture carrying the operator's real
name, home address, employer or a real job-board URL would be published the
moment it is pushed, and no later cleanup un-publishes it. So the corpus is
scanned on every run, before it grows past the point where anyone reads it
whole.
The scan reports its denominator, and that is the point rather than a
courtesy. "Found nothing" is a measurement, and a measurement without a
denominator is indistinguishable from a scan that ran over an empty
directory, matched nothing because its patterns were wrong, or never ran at
all. So every run says how many files and how many bytes it looked at, and
`test_the_scan_can_actually_find` seeds a canary into a temporary corpus and
requires the scan to catch it -- a scanner that cannot find is not evidence of
absence.
Two deliberate deviations from the plan text, both stated rather than
smuggled:
1. The operator's home address is not hard-coded here. Writing a real home
address into a deny list on a public remote would publish the very thing
the list exists to keep out. Private tokens go in the environment variable
`JOBBSOK_HYGIENE_DENY_EXTRA` (newline-separated) instead, and the scan
picks them up if it is set. The name and employer ARE listed literally,
because both already appear in every commit's author metadata on the same
public remote -- listing them costs nothing that is not already public and
buys a check that actually runs.
2. The provenance-header rule covers the directories this step seeds.
`cowork-probe/` is exempt by name: it is the Step 1 probe rig, machine
config with no person in it, and it predates this rule.
Style note: this file follows tests/test_helpers_selfcheck.py.
"""
import os
import re
import warnings
FIXTURES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures")
#: Directories seeded by this step, and the ones the provenance rule governs.
CORPUS_DIRS = ("profiles", "listings")
#: Exempt from the provenance rule, with the reason in the module docstring.
PROVENANCE_EXEMPT = ("cowork-probe",)
#: Literal tokens that must never appear. Real employer names and real job
#: boards; the person tokens are already public in the commit metadata.
DENY_TOKENS = (
"Kjell Tore",
"Guttormsen",
"Statens vegvesen",
"vegvesen",
"finn.no",
"arbeidsplassen.nav.no",
"jobbnorge.no",
"linkedin.com",
"indeed.com",
)
#: Extra deny tokens supplied at run time, one per line. This is where a home
#: address belongs -- checked, never committed.
DENY_EXTRA_ENV = "JOBBSOK_HYGIENE_DENY_EXTRA"
#: Every host and mail domain in the corpus must sit under the reserved
#: `.example` TLD (RFC 2606), so nothing in here can resolve to a real place.
SAFE_TLD = ".example"
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,})")
URL_HOST_RE = re.compile(r"https?://([A-Za-z0-9.:-]+)")
PROVENANCE_MARKER = "Proveniens:"
class KorpusSkannet(UserWarning):
"""Not a defect: the denominator the hygiene scan actually looked at.
Emitted as a warning because pytest shows warnings even under ``-q``,
while a passing test's stdout is captured and never reaches the operator.
"""
def deny_tokens():
tokens = list(DENY_TOKENS)
extra = os.environ.get(DENY_EXTRA_ENV, "")
tokens.extend(line.strip() for line in extra.splitlines() if line.strip())
return tokens
def corpus_files(root):
found = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort()
for name in sorted(filenames):
found.append(os.path.join(dirpath, name))
return found
def scan(root):
"""Scan every file under ``root`` and report findings AND the denominator."""
tokens = deny_tokens()
findings = []
files = corpus_files(root)
total_bytes = 0
for path in files:
with open(path, "r", encoding="utf-8") as handle:
text = handle.read()
total_bytes += len(text.encode("utf-8"))
relative = os.path.relpath(path, root)
lowered = text.lower()
for token in tokens:
if token.lower() in lowered:
findings.append((relative, "deny-token %r" % token))
for domain in EMAIL_RE.findall(text):
if not domain.lower().endswith(SAFE_TLD):
findings.append((relative, "mail domain %r is not %s" % (domain, SAFE_TLD)))
for host in URL_HOST_RE.findall(text):
if not host.lower().split(":")[0].endswith(SAFE_TLD):
findings.append((relative, "url host %r is not %s" % (host, SAFE_TLD)))
return {"files": len(files), "bytes": total_bytes, "findings": findings}
def report(result, root):
message = "hygiene scan: %d files, %d bytes scanned under %s; %d finding(s)" % (
result["files"],
result["bytes"],
os.path.basename(root.rstrip(os.sep)),
len(result["findings"]),
)
print(message)
warnings.warn(message, KorpusSkannet)
return message
def test_every_corpus_fixture_carries_a_provenance_header():
checked = 0
for name in CORPUS_DIRS:
directory = os.path.join(FIXTURES, name)
assert os.path.isdir(directory), "corpus directory %r is missing" % name
files = corpus_files(directory)
assert files, "corpus directory %r is empty" % name
for path in files:
with open(path, "r", encoding="utf-8") as handle:
text = handle.read()
assert PROVENANCE_MARKER in text, "%s has no %r header" % (path, PROVENANCE_MARKER)
checked += 1
# Eight profiles and eight listings, per the plan. A rule that checked
# zero files would pass just as quietly.
assert checked == 16, "expected 16 corpus fixtures, provenance-checked %d" % checked
def test_no_deny_listed_token_appears_anywhere_in_the_corpus():
result = scan(FIXTURES)
assert result["findings"] == [], "hygiene findings: %r" % (result["findings"],)
def test_the_scan_states_the_denominator_it_looked_at():
result = scan(FIXTURES)
message = report(result, FIXTURES)
assert result["files"] >= 16
assert result["bytes"] > 0
assert str(result["files"]) in message and "bytes scanned" in message
def test_the_scan_can_actually_find(tmp_path):
# A canary corpus, deliberately dirty. It lives in tmp_path and never in
# tests/fixtures, because a canary committed to a public remote is the
# leak it is supposed to warn about.
(tmp_path / "cv.md").write_text(
"Proveniens: kanarifugl.\nTidligere hos Statens vegvesen.\n", encoding="utf-8"
)
(tmp_path / "kontakt.md").write_text(
"Proveniens: kanarifugl.\nSkriv til rekruttering@fjordtek.no\n", encoding="utf-8"
)
(tmp_path / "lenke.md").write_text(
"Proveniens: kanarifugl.\nSe https://www.finn.no/job/123\n", encoding="utf-8"
)
result = scan(str(tmp_path))
reasons = " ".join(reason for _path, reason in result["findings"])
assert result["files"] == 3
assert "vegvesen" in reasons
assert "fjordtek.no" in reasons
assert "finn.no" in reasons