The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
468 lines
20 KiB
Python
468 lines
20 KiB
Python
"""The measurements read a FROZEN copy of the knowledge bases, pinned by sha256 — never another
|
|
repository's live build directory.
|
|
|
|
Measured 2026-09-17: another project rebuilt the corpus directory this repository's gate pointed
|
|
straight at. Rows 6-7 went "IKKE MÅLT" and five tests fell, for a change no one here made. The
|
|
failure mode was never falsehood — the gate says IKKE MÅLT and exits non-zero, never green — it was
|
|
that two projects shared a directory neither owns, so what this repository MEASURES could change
|
|
without a commit here.
|
|
|
|
The fix is a copy outside both repositories plus a pin this repository tracks. The pin is the whole
|
|
point: a copy with no pin is the same shared directory one move further away. So the three states
|
|
are separated by construction, and each has its own arm below:
|
|
|
|
* the copy matches the pin -> it resolves, and that is the only green path;
|
|
* the copy is GONE -> ``FrozenBundleMissing`` (an ``OSError``): the gate says
|
|
IKKE MÅLT and fails the exit code exactly as it did before this change; the corpus tests SKIP.
|
|
Since the bases became the package's own fictional examples (``data/kunnskapsbaser/``) the
|
|
default store is always present, so this state is reached only through a user's own store
|
|
(``PORTFOLIO_FROZEN_BUNDLES``) — and it is still pinned below, because that door stays open;
|
|
* the copy DIFFERS from the pin -> ``FrozenBundleDrift`` (a ``ValueError``): loud, named, and
|
|
NEVER a skip. A drifted copy is not an unreadable measurement, it is a measurement of the wrong
|
|
corpus, which is the one thing that produces a silently wrong number.
|
|
|
|
The two exception classes are deliberately unrelated: a caller that catches "missing" to skip must
|
|
not swallow "drift". ``test_drift_is_not_a_missing_copy`` is that gate.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser import frozen_bundles as fb
|
|
from portfolio_optimiser.evals import v1_gate as gate
|
|
from portfolio_optimiser.stress import read_bundle_declarations
|
|
|
|
_REPO = Path(__file__).resolve().parent.parent
|
|
#: Joined at run time on purpose: this file NAMES the forbidden path spellings, and a literal
|
|
#: would make the gate below red against its own source. (Implicit concatenation is not enough —
|
|
#: ``ruff format`` folds ``"a" "b"`` back into one literal, measured here.)
|
|
_OTHER_REPO = "-".join(("another", "project"))
|
|
#: The two spellings a path into ANOTHER repository's build directory takes. Both were present
|
|
#: before this change (3 + 4 hits against the corpus project's build directory, the known
|
|
#: positives recorded in the order's evidence); the patterns are now written for ANY sibling
|
|
#: repository rather than for that one name, which is the rule the order stated. Each is paired
|
|
#: with the line that PROVES it can match: a pattern that matches nothing makes a gate that can
|
|
#: only be green, and the two spellings do not match each other's sample.
|
|
_FORBIDDEN = (
|
|
(
|
|
re.compile(r"~/repos/[\w.-]+/build"),
|
|
f'ROOT = Path("~/repos/{_OTHER_REPO}/build/ferdig")',
|
|
),
|
|
(
|
|
re.compile(r'home\(\) / "repos" / "[\w.-]+" / "build"'),
|
|
'ROOT = Path.home() / "repos" / "' + _OTHER_REPO + '" / "build" / "ferdig"',
|
|
),
|
|
)
|
|
#: Prose that documents history is explicitly allowed by the order; only paths are forbidden.
|
|
_SCANNED = ("src", "tests", "contexts")
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# helpers
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def _bundle(root: Path, body: str = "one") -> Path:
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
(root / "index.md").write_text("# base\n\n- [a](krav/a.md)\n", encoding="utf-8")
|
|
(root / "krav").mkdir(exist_ok=True)
|
|
(root / "krav" / "a.md").write_text(
|
|
f"---\ntype: Krav\ntitle: A\n---\n\n{body}\n", encoding="utf-8"
|
|
)
|
|
return root
|
|
|
|
|
|
def _store(tmp_path: Path, name: str = "driftskrav-2027", body: str = "one") -> tuple[Path, Path]:
|
|
"""A frozen store holding ONE pinned bundle, and the pin file that names it."""
|
|
store = tmp_path / "store"
|
|
digest, files = fb.digest_bundle(_bundle(tmp_path / "src-of-truth", body))
|
|
directory = f"{name}-{digest[: fb.SHORT]}"
|
|
_bundle(store / directory, body)
|
|
pin = tmp_path / "pin.json"
|
|
pin.write_text(
|
|
json.dumps(
|
|
{
|
|
"bundles": {name: {"directory": directory, "sha256": digest, "files": files}},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
return store, pin
|
|
|
|
|
|
def _use(monkeypatch: pytest.MonkeyPatch, store: Path, pin: Path) -> None:
|
|
monkeypatch.setenv(fb.STORE_ENV, str(store))
|
|
monkeypatch.setattr(fb, "PIN_FILE", pin)
|
|
|
|
|
|
def _touch_one_byte(base: Path) -> None:
|
|
doc = base / "krav" / "a.md"
|
|
doc.write_text(doc.read_text(encoding="utf-8").replace("one", "ONE"), encoding="utf-8")
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# (a)-(b) the digest
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_digest_covers_every_byte_and_every_name(tmp_path: Path) -> None:
|
|
"""Stable across recomputation; moved by one byte AND by a rename that changes no byte —
|
|
without the name in the hash, a corpus reshuffled under the same bytes would pin clean."""
|
|
base = _bundle(tmp_path / "b")
|
|
first, files = fb.digest_bundle(base)
|
|
assert (first, files) == fb.digest_bundle(base)
|
|
assert files == 2
|
|
|
|
_touch_one_byte(base)
|
|
changed, _ = fb.digest_bundle(base)
|
|
assert changed != first
|
|
|
|
renamed = _bundle(tmp_path / "c")
|
|
(renamed / "krav" / "a.md").rename(renamed / "krav" / "b.md")
|
|
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("driftskrav-2027") # 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("driftskrav-2027")
|
|
extra.unlink()
|
|
assert fb.bundle_dir("driftskrav-2027") == base # the edit, not the fixture, was the cause
|
|
|
|
(base / "krav" / "a.md").unlink()
|
|
with pytest.raises(fb.FrozenBundleDrift) as exc:
|
|
fb.bundle_dir("driftskrav-2027")
|
|
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:
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
resolved = fb.bundle_dir("driftskrav-2027")
|
|
assert resolved.parent == store
|
|
assert (resolved / "index.md").is_file()
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# (c)-(e) the three states
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_one_changed_byte_is_drift_named_with_both_digests(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
base = fb.bundle_dir("driftskrav-2027") # green before the edit — the control
|
|
expected = json.loads(pin.read_text(encoding="utf-8"))["bundles"]["driftskrav-2027"]["sha256"]
|
|
_touch_one_byte(base)
|
|
|
|
with pytest.raises(fb.FrozenBundleDrift) as exc:
|
|
fb.bundle_dir("driftskrav-2027")
|
|
message = str(exc.value)
|
|
assert "avviker fra pin" in message
|
|
assert "driftskrav-2027" in message
|
|
assert expected[: fb.SHORT] in message # what was pinned
|
|
assert fb.digest_bundle(base)[0][: fb.SHORT] in message # what is on disk
|
|
|
|
|
|
def test_a_missing_copy_is_missing_and_stays_an_oserror(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""``measure_stress`` already catches ``OSError`` -> IKKE MÅLT + exit 1; that path is unchanged."""
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
shutil.rmtree(store / fb.load_pins()["driftskrav-2027"].directory)
|
|
|
|
with pytest.raises(fb.FrozenBundleMissing) as exc:
|
|
fb.bundle_dir("driftskrav-2027")
|
|
assert isinstance(exc.value, OSError)
|
|
assert "driftskrav-2027" in str(exc.value)
|
|
|
|
|
|
def test_drift_is_not_a_missing_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A caller that catches "missing" in order to SKIP must never swallow drift: skipping on a
|
|
wrong corpus is the silent failure this whole change exists to remove."""
|
|
assert not issubclass(fb.FrozenBundleDrift, fb.FrozenBundleMissing)
|
|
assert not issubclass(fb.FrozenBundleMissing, fb.FrozenBundleDrift)
|
|
assert not isinstance(fb.FrozenBundleDrift("x"), OSError)
|
|
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
_touch_one_byte(fb.bundle_dir("driftskrav-2027"))
|
|
with pytest.raises(fb.FrozenBundleDrift):
|
|
try:
|
|
fb.bundle_dir("driftskrav-2027")
|
|
except fb.FrozenBundleMissing: # pragma: no cover - the defect this arm forbids
|
|
pytest.fail("drift was answered as a missing copy")
|
|
|
|
|
|
def test_an_unknown_name_is_refused_by_name(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
with pytest.raises(fb.FrozenBundleMissing) as exc:
|
|
fb.bundle_dir("prosesskatalog-2027")
|
|
assert "prosesskatalog-2027" in str(exc.value) and "driftskrav-2027" in str(exc.value)
|
|
|
|
|
|
def test_an_explicit_override_is_unpinned_and_the_operator_named_it(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""``--bundle-root`` / ``PORTFOLIO_BUNDLE_ROOT`` stays an escape hatch: the operator who
|
|
names a live mount gets it, pin or no pin. Absent it, the frozen store answers."""
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
live = tmp_path / "live"
|
|
_bundle(live / "driftskrav-2027", body="something else entirely")
|
|
assert fb.bundle_dir("driftskrav-2027", override=live) == live / "driftskrav-2027"
|
|
monkeypatch.setenv(fb.OVERRIDE_ENV, str(live))
|
|
assert fb.bundle_dir("driftskrav-2027") == live / "driftskrav-2027"
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# (f)-(g) the tracked pin
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_pin_names_every_base_the_context_sets_declare() -> None:
|
|
declared = {
|
|
d["name"]
|
|
for path in sorted((_REPO / "contexts").glob("*/bundle.txt"))
|
|
for d in read_bundle_declarations(path)
|
|
}
|
|
assert declared, "no context set declares a base — the denominator would be vacuous"
|
|
assert declared <= set(fb.load_pins())
|
|
|
|
|
|
def test_the_pinned_directory_name_carries_the_short_digest() -> None:
|
|
"""A stale copy is visible in ``ls``, not only to the verifier."""
|
|
pins = fb.load_pins()
|
|
assert pins, "the pin file names no bundle"
|
|
for name, pin in pins.items():
|
|
assert pin.directory == f"{name}-{pin.sha256[: fb.SHORT]}"
|
|
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_default_store_is_the_packaged_example_bases(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The pinned bases are the package's OWN fictional examples, so the default store sits next
|
|
to the module — a wheel finds it without a checkout — and every pin resolves there, matched,
|
|
with nothing in the environment helping.
|
|
|
|
Three halves, each a defect of its own: a default pointing at a machine-local path (the old
|
|
``~/corpora`` store) makes every corpus arm skip on every other machine; a pin that does not
|
|
resolve from the default is a pin nothing checks; and a copy left in the store with no pin is
|
|
a stale base that ``ls`` shows and nothing measures. The pin file itself still carries only
|
|
the pin, never bundle bytes.
|
|
"""
|
|
monkeypatch.delenv(fb.STORE_ENV, raising=False)
|
|
monkeypatch.delenv(fb.OVERRIDE_ENV, raising=False)
|
|
packaged = Path(fb.__file__).resolve().parent / "data" / "kunnskapsbaser"
|
|
assert Path(fb.DEFAULT_STORE).resolve() == packaged
|
|
pins = fb.load_pins()
|
|
assert set(pins) == {"driftskrav-2027", "prosesskatalog-2027"}
|
|
for name, pin in pins.items():
|
|
base = fb.bundle_dir(name)
|
|
assert base == Path(fb.DEFAULT_STORE) / pin.directory
|
|
assert fb.digest_bundle(base)[1] == pin.files
|
|
on_disk = sorted(p.name for p in packaged.iterdir() if p.is_dir())
|
|
assert on_disk == sorted(pin.directory for pin in pins.values()), on_disk
|
|
tracked = (_REPO / "src" / "portfolio_optimiser" / "frozen_bundles.json").read_text("utf-8")
|
|
assert "index.md" not in tracked
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# (h) nothing reads the other repository's build directory any more
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
#: 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, scanned = _forbidden_hits(_REPO, _SCANNED)
|
|
assert scanned > 0
|
|
assert hits == [], "\n".join(hits)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# (i)-(j) the gate
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def _evidence(tmp_path: Path) -> tuple[dict[str, Any], Path]:
|
|
stress_root = tmp_path / "stress"
|
|
(stress_root / "o").mkdir(parents=True)
|
|
return {
|
|
"label": "s",
|
|
"root": "scratchpad",
|
|
"runs": [
|
|
{
|
|
"context": "contexts/serverrom-2027",
|
|
"outbox": "o",
|
|
"run_id": "r",
|
|
"bundle": None,
|
|
}
|
|
],
|
|
}, stress_root
|
|
|
|
|
|
def test_a_drifted_copy_fails_the_gate_with_the_reason_said(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
_touch_one_byte(fb.bundle_dir("driftskrav-2027"))
|
|
evidence, stress_root = _evidence(tmp_path)
|
|
|
|
m = gate.measure_stress(evidence, _REPO, stress_root, None)
|
|
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
|
|
|
|
|
|
def test_a_missing_copy_is_not_measured_and_never_green(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
evidence, stress_root = _evidence(tmp_path)
|
|
monkeypatch.setenv(fb.STORE_ENV, str(tmp_path / "gone"))
|
|
|
|
m = gate.measure_stress(evidence, _REPO, stress_root, None)
|
|
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
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# (k) the delivered-corpus tests: missing SKIPS, drift FAILS
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"module,helper",
|
|
[
|
|
("test_context_sets_loadbearing", "_bundle_dir"),
|
|
("test_navigation_window_loadbearing", "_delivered"),
|
|
("test_inert_identifier_loadbearing", "_base"),
|
|
("test_requirement_number_gate_loadbearing", "_base"),
|
|
],
|
|
)
|
|
def test_the_corpus_tests_skip_when_absent_but_fail_on_drift(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, module: str, helper: str
|
|
) -> None:
|
|
mod = __import__(module)
|
|
resolve = getattr(mod, helper)
|
|
store, pin = _store(tmp_path)
|
|
_use(monkeypatch, store, pin)
|
|
|
|
assert resolve("driftskrav-2027").is_dir() # control: the matching copy resolves
|
|
_touch_one_byte(fb.bundle_dir("driftskrav-2027"))
|
|
# A skip is caught EXPLICITLY, never left to ``pytest.raises``: measured against the mutation
|
|
# that makes drift a subclass of missing, these four arms SKIPPED instead of failing (5 -> 9
|
|
# skipped over the whole suite) and stayed green — a gate that cannot see the one defect it
|
|
# exists for.
|
|
try:
|
|
resolve("driftskrav-2027")
|
|
except fb.FrozenBundleDrift:
|
|
pass
|
|
except pytest.skip.Exception as exc:
|
|
raise AssertionError(f"{module}.{helper} SKIPPED a drifted copy: {exc}") from None
|
|
else:
|
|
raise AssertionError(f"{module}.{helper} accepted a drifted copy")
|
|
|
|
monkeypatch.setenv(fb.STORE_ENV, str(tmp_path / "gone"))
|
|
with pytest.raises(pytest.skip.Exception):
|
|
resolve("driftskrav-2027")
|