test(frozen-pin): an added or removed file, and listing order, pinned by tests; drift named apart from missing
K5 survived a full run (a mutant ignoring an extra or removed file left the suite green) and K4 (sort removed) was caught only by corpus data. Now pinned with no corpus: the add arm, the remove arm, and a shuffled directory listing digesting the same. The gate says "pin-drift" for a drifted copy instead of "artefakter mangler"; the unread "store" key is gone from frozen_bundles.json; the forbidden-path scan covers md/yaml/yml/toml and proves a known positive per suffix. Five mutants killed in a scratch clone (drop-last-entry 8 failed, no-sort, no-names, drift-as-missing, scan-suffixes-narrowed 1 failed each). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a13b905b0c
commit
68079469c3
3 changed files with 108 additions and 17 deletions
|
|
@ -86,7 +86,6 @@ def _store(tmp_path: Path, name: str = "n500-2024", body: str = "one") -> tuple[
|
|||
pin.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"store": str(store),
|
||||
"bundles": {name: {"directory": directory, "sha256": digest, "files": files}},
|
||||
}
|
||||
),
|
||||
|
|
@ -127,6 +126,48 @@ def test_the_digest_covers_every_byte_and_every_name(tmp_path: Path) -> None:
|
|||
assert fb.digest_bundle(renamed)[0] != fb.digest_bundle(_bundle(tmp_path / "d"))[0]
|
||||
|
||||
|
||||
def test_an_added_or_removed_file_is_drift_even_when_no_kept_byte_changes(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The two arms nothing pinned (measured 2026-09-18: a mutant that ignored an extra or a
|
||||
missing file left 197 tests green). Every surviving file is byte-identical to the pin here —
|
||||
only the SET of files moved, and the set is what a copy is."""
|
||||
store, pin = _store(tmp_path)
|
||||
_use(monkeypatch, store, pin)
|
||||
base = fb.bundle_dir("n500-2024") # control: green before either edit
|
||||
|
||||
extra = base / "krav" / "b.md"
|
||||
extra.write_text("---\ntype: Krav\ntitle: B\n---\n", encoding="utf-8")
|
||||
with pytest.raises(fb.FrozenBundleDrift):
|
||||
fb.bundle_dir("n500-2024")
|
||||
extra.unlink()
|
||||
assert fb.bundle_dir("n500-2024") == base # the edit, not the fixture, was the cause
|
||||
|
||||
(base / "krav" / "a.md").unlink()
|
||||
with pytest.raises(fb.FrozenBundleDrift) as exc:
|
||||
fb.bundle_dir("n500-2024")
|
||||
assert "1 filer" in str(exc.value) # what disk holds now: one file of the pinned two
|
||||
|
||||
|
||||
def test_the_digest_does_not_depend_on_directory_listing_order(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Determinism with no corpus mounted: the same tree listed in ANOTHER order digests the same.
|
||||
Only the corpus tests used to catch a removed sort, and they skip where no corpus exists."""
|
||||
base = _bundle(tmp_path / "b")
|
||||
for extra in ("z.md", "m.md", "krav/q.md"):
|
||||
(base / extra).write_text(extra, encoding="utf-8")
|
||||
baseline = fb.digest_bundle(base)
|
||||
listed = list(base.rglob("*"))
|
||||
assert len(listed) > 3
|
||||
|
||||
real = Path.rglob
|
||||
for order in (list(reversed(listed)), listed[1:] + listed[:1]):
|
||||
monkeypatch.setattr(Path, "rglob", lambda self, pattern, _o=order: iter(_o))
|
||||
assert fb.digest_bundle(base) == baseline
|
||||
monkeypatch.setattr(Path, "rglob", real)
|
||||
|
||||
|
||||
def test_a_matching_copy_resolves_to_the_pinned_directory(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
|
@ -239,6 +280,13 @@ def test_the_pinned_directory_name_carries_the_short_digest() -> None:
|
|||
assert pin.files > 0
|
||||
|
||||
|
||||
def test_the_pin_file_carries_no_key_nobody_reads() -> None:
|
||||
"""``store`` sat in the pin file with no reader: the store is ``DEFAULT_STORE`` / the env var,
|
||||
and a second, silent spelling of it would drift from the first."""
|
||||
keys = set(json.loads(fb.PIN_FILE.read_text(encoding="utf-8")))
|
||||
assert keys == {"source", "renewal", "bundles"}
|
||||
|
||||
|
||||
def test_the_bundle_itself_is_never_tracked_here() -> None:
|
||||
"""Vegnormal corpora must not reach a public remote: only the pin is tracked."""
|
||||
tracked = (_REPO / "src" / "portfolio_optimiser" / "frozen_bundles.json").read_text("utf-8")
|
||||
|
|
@ -250,19 +298,49 @@ def test_the_bundle_itself_is_never_tracked_here() -> None:
|
|||
# ---------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
#: Every text form a path to that build directory could be written in. ``.py/.json/.txt`` alone
|
||||
#: left prose-and-config (md, yaml, toml) outside the denominator, unmeasured.
|
||||
_SUFFIXES = {".py", ".json", ".txt", ".md", ".yaml", ".yml", ".toml"}
|
||||
|
||||
|
||||
def _forbidden_hits(root: Path, tops: tuple[str, ...]) -> tuple[list[str], int]:
|
||||
"""(hits, files scanned). The count is the denominator: a scan of zero files is not green."""
|
||||
hits: list[str] = []
|
||||
scanned = 0
|
||||
for top in tops:
|
||||
for path in sorted((root / top).rglob("*")):
|
||||
if not path.is_file() or path.suffix not in _SUFFIXES:
|
||||
continue
|
||||
scanned += 1
|
||||
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if any(p.search(line) for p, _ in _FORBIDDEN):
|
||||
hits.append(f"{path.relative_to(root)}:{n}: {line.strip()}")
|
||||
return hits, scanned
|
||||
|
||||
|
||||
def test_the_forbidden_path_scan_finds_a_known_positive_in_every_scanned_suffix(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Shown, not assumed: plant each spelling in a file of EACH suffix and see the scan report
|
||||
exactly those files. A suffix the scan skips would read clean here and be red in this test."""
|
||||
suffixes = (".py", ".json", ".txt", ".md", ".yaml", ".yml", ".toml") # literal, not _SUFFIXES
|
||||
for suffix in suffixes:
|
||||
for i, (_, sample) in enumerate(_FORBIDDEN):
|
||||
planted = tmp_path / "src" / f"planted{i}{suffix}"
|
||||
planted.parent.mkdir(exist_ok=True)
|
||||
planted.write_text(f"before\n{sample}\nafter\n", encoding="utf-8")
|
||||
hits, scanned = _forbidden_hits(tmp_path, ("src",))
|
||||
assert scanned == len(suffixes) * len(_FORBIDDEN)
|
||||
assert len(hits) == scanned, hits
|
||||
|
||||
|
||||
def test_no_measurement_reads_the_other_repos_build_directory() -> None:
|
||||
"""Both spellings, each with a known-positive control: a pattern that cannot match anything
|
||||
is a gate that can only be green."""
|
||||
for pattern, known_positive in _FORBIDDEN:
|
||||
assert pattern.search(known_positive), pattern.pattern
|
||||
hits = [
|
||||
f"{path.relative_to(_REPO)}:{n}: {line.strip()}"
|
||||
for top in _SCANNED
|
||||
for path in sorted((_REPO / top).rglob("*"))
|
||||
if path.is_file() and path.suffix in {".py", ".json", ".txt"}
|
||||
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
|
||||
if any(p.search(line) for p, _ in _FORBIDDEN)
|
||||
]
|
||||
hits, scanned = _forbidden_hits(_REPO, _SCANNED)
|
||||
assert scanned > 0
|
||||
assert hits == [], "\n".join(hits)
|
||||
|
||||
|
||||
|
|
@ -297,9 +375,12 @@ def test_a_drifted_copy_fails_the_gate_with_the_reason_said(
|
|||
evidence, stress_root = _evidence(tmp_path)
|
||||
|
||||
m = gate.measure_stress(evidence, _REPO, stress_root, None)
|
||||
assert "avviker fra pin" in m.missing
|
||||
assert "avviker fra pin" in m.drift
|
||||
assert m.missing == "" # drift is NOT "artefakter mangler" — the two are named apart
|
||||
row = gate.score_undeclared(["p"], {"p": "passed"}, m, "s")
|
||||
assert (row.k, row.status, row.failing) == (None, gate.NOT_MEASURED, True)
|
||||
assert "pin-drift" in row.reason and "artefakter mangler" not in row.reason
|
||||
assert "pin-drift" in gate.score_named(m, "s").reason
|
||||
assert gate.exit_code([row]) == 1
|
||||
|
||||
|
||||
|
|
@ -312,9 +393,10 @@ def test_a_missing_copy_is_not_measured_and_never_green(
|
|||
monkeypatch.setenv(fb.STORE_ENV, str(tmp_path / "gone"))
|
||||
|
||||
m = gate.measure_stress(evidence, _REPO, stress_root, None)
|
||||
assert m.missing and m.validated == 0
|
||||
assert m.missing and not m.drift and m.validated == 0
|
||||
row = gate.score_undeclared(["p"], {"p": "passed"}, m, "s")
|
||||
assert (row.status, row.failing) == (gate.NOT_MEASURED, True)
|
||||
assert "artefakter mangler" in row.reason and "pin-drift" not in row.reason
|
||||
assert gate.exit_code([row]) == 1
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue