Three of the new row-3 arms pinned 512 files and the literal source label, which only a tree WITH git metadata can answer; a clean extract (git archive | tar -x) would have turned all three red for the same reason the three already-named extraction artefacts go red. The property they test holds in both worlds, so they now assert it there: main.py, examples/, spikes/ and contexts/ are inside the guard whichever manifest was read, and the row prints whatever denominator it measured. The exact number and the label belong to ONE arm — the one whose subject IS the git manifest. In an extract it is an extraction artefact of the same class as the others, and says so in its docstring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1011 lines
44 KiB
Python
1011 lines
44 KiB
Python
"""B-gatens egne tester: hver rad KAN bli grønn og KAN bli rød — og kan ikke FAKES grønn.
|
|
|
|
Rammen er operatørbeslutningen 19.09.2026: i utvikling og test LEDER Claude Code, og
|
|
portfolio-optimiser er verktøykassen. po kaller ALDRI Claude. Gaten måler avstanden dit, rad for
|
|
rad, uten et eneste modellkall.
|
|
|
|
Tre ting gjør testene load-bearing i stedet for grønne-men-døde:
|
|
|
|
* **Nevneren telles uavhengig HER.** Rad 1 og rad 2 får ikke sine tall fra gatens egne funksjoner:
|
|
testen parser kilden selv (``ast``) eller importerer modulen og teller, og sammenligner. En gate
|
|
som hardkoder nevneren felles av arm ``M-1``, som kjører den mot en KONSTRUERT kilde der svaret
|
|
er et annet enn repoets.
|
|
* **Hver vakt har en kjent-positiv.** Et mønster som ikke treffer sin egen kjent-positive er ikke
|
|
en måling, og rad 3 nekter å telle det (``M-3``). Et fravær («0 treff») uttales med nevner.
|
|
* **Rad 6 kan ikke bli grønn av en fil gaten selv skriver** (``M-5``): armen kjører hele
|
|
``evaluate`` og krever at attesteringsfila fortsatt ikke finnes etterpå.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from portfolio_optimiser import workflow
|
|
from portfolio_optimiser.evals import b_gate as gate
|
|
from portfolio_optimiser.evals import v1_gate
|
|
|
|
_REPO = Path(__file__).resolve().parents[1]
|
|
_SRC = _REPO / "src" / "portfolio_optimiser"
|
|
_CONFIG = gate.load_config()
|
|
#: Klokka rad 6 leses mot, injisert: den ENE sjekken som ser på tiden skal ikke flytte seg.
|
|
_NOW = datetime(2026, 9, 19, 12, 0, tzinfo=timezone.utc)
|
|
|
|
#: Repoets egne tall ved skriving (19.09.2026), talt av armene under mot KILDEN. De står her for
|
|
#: at en stille endring i kjørestien skal vise seg som en rød test og ikke som et nytt tall.
|
|
_STEPS_TODAY = 17
|
|
_EXTERNAL_TODAY = 3
|
|
_ROLES_TODAY = ("proposer", "checker")
|
|
_PATTERNS_TODAY = 9
|
|
|
|
|
|
def _write(path: Path, text: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
|
|
|
|
def _probes(outcome: str = "missing") -> Any:
|
|
return lambda nodeids: {n: outcome for n in nodeids}
|
|
|
|
|
|
def _row(rows: list[v1_gate.Row], key: str) -> v1_gate.Row:
|
|
return next(r for r in rows if r.key == key)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Rad 1 — verktøykassen er komplett
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def _independently_counted_steps() -> tuple[int, int]:
|
|
"""(steg som oppløses i kilden, steg med et kallsted) — talt HER, av testens egen parser.
|
|
|
|
Kilden er ``src/portfolio_optimiser/``; ingen av gatens funksjoner røres."""
|
|
resolved = called = 0
|
|
for step in _CONFIG["steps"]:
|
|
tree = ast.parse((_SRC / step["module"]).read_text(encoding="utf-8"))
|
|
defs = {
|
|
n.name
|
|
for n in tree.body
|
|
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
|
|
}
|
|
if step["symbol"] in defs:
|
|
resolved += 1
|
|
driver = ast.parse((_SRC / step["driver"]["module"]).read_text(encoding="utf-8"))
|
|
scopes = [
|
|
n
|
|
for n in ast.walk(driver)
|
|
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
and n.name == step["driver"]["scope"]
|
|
]
|
|
names = {
|
|
c.func.id if isinstance(c.func, ast.Name) else c.func.attr
|
|
for s in scopes
|
|
for c in ast.walk(s)
|
|
if isinstance(c, ast.Call) and isinstance(c.func, (ast.Name, ast.Attribute))
|
|
}
|
|
if step["symbol"] in names:
|
|
called += 1
|
|
return resolved, called
|
|
|
|
|
|
def test_the_step_denominator_equals_an_independent_count_of_the_source() -> None:
|
|
resolved, called = _independently_counted_steps()
|
|
assert resolved == called == _STEPS_TODAY, "kjørestien har endret seg — oppdater nevneren"
|
|
row = gate.score_toolbox(_CONFIG["steps"], _SRC, _CONFIG["run_path"], {}, _REPO)
|
|
assert row.n == _STEPS_TODAY
|
|
|
|
|
|
def test_row1_today_is_red_with_three_of_seventeen_steps_callable_from_outside() -> None:
|
|
passing = {n: "passed" for step in _CONFIG["steps"] for n in step.get("probe", ())}
|
|
row = gate.score_toolbox(_CONFIG["steps"], _SRC, _CONFIG["run_path"], passing, _REPO)
|
|
assert (row.k, row.n, row.status) == (_EXTERNAL_TODAY, _STEPS_TODAY, gate.RED)
|
|
named = {x.split(":")[0] for x in row.exceptions}
|
|
assert "rundebinding" not in named and "gate" not in named
|
|
assert "validering" in named and "utboks" in named
|
|
assert "prepass-artefakt" in named and "parse-feil" in named
|
|
|
|
|
|
def _fake_src(root: Path, *, chat_client: bool, symbol: str = "gjoer_noe") -> Path:
|
|
"""En konstruert kilde med ETT steg: en inngang ``main`` som når steget.
|
|
|
|
``chat_client=True`` legger en chatklient på veien dit — steget er da fortsatt kallbart, men
|
|
ikke uten en modell, og skal ikke telle."""
|
|
src = root / "fakesrc"
|
|
body = " return create_chat_client()\n" if chat_client else " return 1\n"
|
|
_write(src / "steg.py", f"def {symbol}():\n return 42\n")
|
|
_write(
|
|
src / "driver.py",
|
|
"from fakesrc.steg import " + symbol + "\n\n"
|
|
"def _hjelper():\n" + body + "\n\n"
|
|
"def kjor():\n"
|
|
f" _hjelper()\n return {symbol}()\n\n\n"
|
|
"def main(argv=None):\n return kjor()\n\n\n"
|
|
'if __name__ == "__main__":\n raise SystemExit(main())\n',
|
|
)
|
|
return src
|
|
|
|
|
|
#: En dør er registrert OG bevist med atferd. De konstruerte kildene under bærer begge deler.
|
|
_FAKE_PROBE = "tests/test_konstruert.py::test_doeren_skriver_artefaktet"
|
|
_FAKE_PASSED = {_FAKE_PROBE: "passed"}
|
|
|
|
|
|
def _one_step(symbol: str = "gjoer_noe") -> list[dict[str, Any]]:
|
|
return [
|
|
{
|
|
"id": "steg",
|
|
"label": "ett steg",
|
|
"module": "steg.py",
|
|
"symbol": symbol,
|
|
"driver": {"module": "driver.py", "scope": "kjor"},
|
|
"entry": {"kind": "module-main", "module": "driver.py", "scope": "main"},
|
|
"probe": [_FAKE_PROBE],
|
|
}
|
|
]
|
|
|
|
|
|
def test_a_step_reachable_from_a_cli_entry_without_a_chat_client_counts(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
row = gate.score_toolbox(_one_step(), src, None, _FAKE_PASSED, tmp_path)
|
|
assert (row.k, row.n, row.status) == (1, 1, gate.GREEN)
|
|
|
|
|
|
def test_m2_a_step_that_needs_a_chat_client_is_not_callable_from_outside(tmp_path: Path) -> None:
|
|
"""M-2: fjernes chatklient-sjekken, hopper k fra 0 til 1 og denne armen feller mutanten."""
|
|
src = _fake_src(tmp_path, chat_client=True)
|
|
row = gate.score_toolbox(_one_step(), src, None, _FAKE_PASSED, tmp_path)
|
|
assert (row.k, row.n, row.status) == (0, 1, gate.RED)
|
|
assert any("chatklient" in x for x in row.exceptions)
|
|
|
|
|
|
def test_m1_the_denominator_follows_the_source_and_is_not_the_config_length(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""M-1: en hardkodet nevner overlever repoet, men ikke en kilde med ett steg mindre."""
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
steps = _one_step() + [
|
|
{
|
|
"id": "borte",
|
|
"label": "et steg som ikke finnes i kilden",
|
|
"module": "steg.py",
|
|
"symbol": "finnes_ikke",
|
|
"driver": {"module": "driver.py", "scope": "kjor"},
|
|
"entry": {"kind": "module-main", "module": "driver.py", "scope": "main"},
|
|
}
|
|
]
|
|
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
|
|
assert row.n == 1, "et steg uten symbol i kilden er ikke et steg"
|
|
assert any("borte" in x for x in row.exceptions)
|
|
|
|
|
|
def test_a_step_the_driver_no_longer_calls_leaves_the_denominator(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
_write(src / "steg.py", "def gjoer_noe():\n return 42\n\n\ndef ubrukt():\n return 0\n")
|
|
steps = _one_step() + [
|
|
{
|
|
"id": "ubrukt",
|
|
"label": "definert, men ikke i kjørestien",
|
|
"module": "steg.py",
|
|
"symbol": "ubrukt",
|
|
"driver": {"module": "driver.py", "scope": "kjor"},
|
|
"entry": {"kind": "module-main", "module": "driver.py", "scope": "main"},
|
|
}
|
|
]
|
|
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
|
|
assert row.n == 1
|
|
|
|
|
|
def test_an_entry_without_a_main_is_no_entry(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
text = (src / "driver.py").read_text(encoding="utf-8").replace("def main(", "def _main(")
|
|
_write(src / "driver.py", text)
|
|
row = gate.score_toolbox(_one_step(), src, None, _FAKE_PASSED, tmp_path)
|
|
assert (row.k, row.n) == (0, 1)
|
|
assert any("main" in x for x in row.exceptions)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Rad 2 — det modellen leverte, kan leveres utenfra
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_role_denominator_is_read_from_workflow_and_matches_the_imported_module() -> None:
|
|
assert workflow._MAKER_CHECKER_ROLES == _ROLES_TODAY # uavhengig telling: modulen selv
|
|
assert gate.run_roles(_SRC, _CONFIG["roles"]["source"]) == _ROLES_TODAY
|
|
|
|
|
|
def test_the_role_denominator_follows_a_changed_source(tmp_path: Path) -> None:
|
|
src = tmp_path / "fakesrc"
|
|
_write(src / "workflow.py", '_MAKER_CHECKER_ROLES = ("a", "b", "c")\n')
|
|
assert gate.run_roles(src, _CONFIG["roles"]["source"]) == ("a", "b", "c")
|
|
|
|
|
|
def test_a_role_counts_only_when_every_named_probe_passes() -> None:
|
|
evidence = _CONFIG["roles"]["evidence"]
|
|
outcomes = {n: "passed" for ns in evidence.values() for n in ns}
|
|
row = gate.score_roles(_CONFIG["roles"], outcomes, _SRC)
|
|
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN)
|
|
outcomes[evidence["checker"][0]] = "failed"
|
|
row = gate.score_roles(_CONFIG["roles"], outcomes, _SRC)
|
|
assert (row.k, row.n, row.status) == (1, 2, gate.RED)
|
|
|
|
|
|
def test_row2_today_is_red_because_the_named_probes_do_not_exist_yet() -> None:
|
|
row = gate.score_roles(_CONFIG["roles"], {}, _SRC)
|
|
assert (row.k, row.n, row.status) == (0, 2, gate.RED)
|
|
assert all("missing" in x for x in row.exceptions)
|
|
|
|
|
|
def test_m4_row2_demands_byte_identical_artefacts_not_merely_present_ones() -> None:
|
|
"""M-4: byttes kravet til «finnes», står kontrakten igjen uten den ene setningen som skiller
|
|
en ekte likhet fra to filer med samme navn."""
|
|
assert "byte-identisk" in gate.BYTE_IDENTICAL_RULE
|
|
row = gate.score_roles(_CONFIG["roles"], {}, _SRC)
|
|
assert any("byte-identisk" in a for a in row.attests)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Rad 3 — po har ingen vei til Claude
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def _patterns() -> list[dict[str, str]]:
|
|
return list(_CONFIG["no_claude_path"]["patterns"])
|
|
|
|
|
|
def test_every_pattern_hits_its_own_known_positive_and_misses_its_known_negative() -> None:
|
|
"""Fravær er et måleresultat: en vakt som ikke kan treffe, måler ikke null — den måler
|
|
ingenting. Nevner: alle mønstrene i kontrakten."""
|
|
assert len(_patterns()) == _PATTERNS_TODAY
|
|
for spec in _patterns():
|
|
rx = re.compile(gate.decode(spec["pattern_b64"]))
|
|
assert rx.search(gate.decode(spec["known_positive_b64"])), spec["id"]
|
|
assert not rx.search(gate.decode(spec["known_negative_b64"])), spec["id"]
|
|
|
|
|
|
def _clean_surface(tmp_path: Path) -> dict[str, Any]:
|
|
"""En ren, konstruert flate med sin egen sentinel — nevneren er aldri 0 her."""
|
|
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
|
|
cfg["manifest"] = {"sentinel": "src/rent.py", "skip_dirs": [".git", "__pycache__"]}
|
|
_write(tmp_path / "src" / "rent.py", "print('ingenting her')\n")
|
|
return cfg
|
|
|
|
|
|
def test_m3_the_denominator_is_the_number_of_patterns_that_can_measure(tmp_path: Path) -> None:
|
|
"""M-3: fjernes et mønster, faller nevneren — og et mønster som ikke treffer sin egen
|
|
kjent-positive telles ikke som en vakt."""
|
|
cfg = _clean_surface(tmp_path)
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert (row.k, row.n, row.status) == (_PATTERNS_TODAY, _PATTERNS_TODAY, gate.GREEN)
|
|
|
|
cfg["patterns"][0]["pattern_b64"] = base64.b64encode(b"finnes-aldri-xyzzy").decode()
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert (row.k, row.n, row.status) == (_PATTERNS_TODAY - 1, _PATTERNS_TODAY, gate.RED)
|
|
assert any("kjent-positiv" in x for x in row.exceptions)
|
|
|
|
cfg["patterns"] = cfg["patterns"][1:]
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert row.n == _PATTERNS_TODAY - 1
|
|
|
|
|
|
def test_a_planted_path_to_claude_turns_the_row_red_for_that_pattern(tmp_path: Path) -> None:
|
|
cfg = _clean_surface(tmp_path)
|
|
assert gate.score_no_claude_path(cfg, tmp_path).status == gate.GREEN, "rc-0-kontrollen først"
|
|
for spec in cfg["patterns"]:
|
|
_write(
|
|
tmp_path / "src" / f"{spec['id']}.py", gate.decode(spec["known_positive_b64"]) + "\n"
|
|
)
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert (row.k, row.status) == (0, gate.RED)
|
|
assert len(row.exceptions) == _PATTERNS_TODAY
|
|
|
|
|
|
def test_markdown_counts_only_inside_fenced_blocks(tmp_path: Path) -> None:
|
|
"""«docs med kjørbare kommandoer»: en pakkenavn-omtale i brødtekst er ikke en vei til Claude;
|
|
den samme linja i en kodeblokk er en kommando noen kan kjøre."""
|
|
cfg = _clean_surface(tmp_path)
|
|
spec = next(s for s in cfg["patterns"] if s["id"] == "maf-anthropic")
|
|
line = gate.decode(spec["known_positive_b64"])
|
|
_write(tmp_path / "docs" / "prosa.md", f"- vurdert og forkastet: `{line}` (aldri tatt inn)\n")
|
|
assert gate.score_no_claude_path(cfg, tmp_path).status == gate.GREEN
|
|
_write(tmp_path / "docs" / "kjorbar.md", f"```bash\n{line}\n```\n")
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert row.status == gate.RED
|
|
assert any("kjorbar.md" in x for x in row.exceptions)
|
|
|
|
|
|
def test_row3_on_the_real_published_surface_is_green_today() -> None:
|
|
row = gate.score_no_claude_path(_CONFIG["no_claude_path"], _REPO)
|
|
assert (row.k, row.n, row.status) == (_PATTERNS_TODAY, _PATTERNS_TODAY, gate.GREEN), (
|
|
row.exceptions
|
|
)
|
|
|
|
|
|
def test_the_surface_names_its_denominator_in_files_not_in_prose() -> None:
|
|
files, _ = gate.published_files(_REPO, _manifest())
|
|
assert len(files) > 100, "en tom flate ville gitt 0 treff uten å måle noe"
|
|
assert any(f.name == "run.py" for f in files)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Rad 4 — ingen modellkall i verktøykasse-modus
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_row4_is_red_today_and_names_all_three_checks() -> None:
|
|
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], {}, _SRC)
|
|
assert (row.k, row.n, row.status) == (0, 3, gate.RED)
|
|
assert len(row.exceptions) == 3
|
|
|
|
|
|
def test_row4_counts_only_checks_whose_probes_pass() -> None:
|
|
checks = _CONFIG["no_model_calls"]["checks"]
|
|
outcomes = {n: "passed" for c in checks.values() for n in c["probe"]}
|
|
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], outcomes, _SRC)
|
|
assert row.status == gate.GREEN
|
|
outcomes[checks["fullfører"]["probe"][0]] = "skipped"
|
|
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], outcomes, _SRC)
|
|
assert (row.k, row.status) == (2, gate.RED)
|
|
|
|
|
|
def test_row4_states_what_the_budget_guard_is_in_b_instead_of_pretending() -> None:
|
|
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], {}, _SRC)
|
|
assert any("budsjett" in a.casefold() for a in row.attests)
|
|
assert "valgt" in gate.BUDGET_IN_B.casefold()
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Rad 5 — Foundry-veien urørt
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_foundry_path_is_intact_in_the_source_today() -> None:
|
|
ok, missing = gate.foundry_intact(_CONFIG["foundry"], _SRC)
|
|
assert (ok, missing) == (True, ())
|
|
|
|
|
|
def test_the_foundry_check_fails_when_the_azure_profile_is_gone(tmp_path: Path) -> None:
|
|
src = tmp_path / "fakesrc"
|
|
_write(src / "backends.py", "class Profile:\n LOCAL = 'local'\n")
|
|
_write(src / "run.py", "def run_project(client_factory=None):\n return 1\n")
|
|
ok, missing = gate.foundry_intact(_CONFIG["foundry"], src)
|
|
assert ok is False
|
|
assert any("AZURE" in m for m in missing)
|
|
|
|
|
|
def test_row5_is_red_until_the_schema_comparison_probe_passes() -> None:
|
|
structural = len(_CONFIG["foundry"]["profile"]["members"]) + 2
|
|
row = gate.score_foundry(_CONFIG["foundry"], {}, _SRC)
|
|
assert (row.k, row.n, row.status) == (structural, structural + 1, gate.RED)
|
|
nodeid = _CONFIG["foundry"]["evidence"]["samme-skjema"][0]
|
|
row = gate.score_foundry(_CONFIG["foundry"], {nodeid: "passed"}, _SRC)
|
|
assert (row.k, row.n, row.status) == (structural + 1, structural + 1, gate.GREEN)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Rad 6 — kjøreboka finnes
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_row6_is_not_measured_while_the_runbook_is_missing(tmp_path: Path) -> None:
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path)
|
|
assert (row.k, row.n, row.status) == (0, 2, gate.NOT_MEASURED)
|
|
assert row.failing is True
|
|
|
|
|
|
def test_row6_is_still_not_measured_without_the_operators_attestation(tmp_path: Path) -> None:
|
|
_write(tmp_path / _CONFIG["runbook"]["path"], "# kjørebok\n\nsteg 1 …\n")
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path)
|
|
assert (row.k, row.n, row.status) == (1, 2, gate.NOT_MEASURED)
|
|
|
|
|
|
def test_row6_goes_green_only_with_every_required_key(tmp_path: Path) -> None:
|
|
cfg = _CONFIG["runbook"]
|
|
digest = _runbook(tmp_path)
|
|
attest = tmp_path / cfg["attestation"]
|
|
_write(attest, f"kjørebok: {cfg['path']}\nsjekksum: {digest}\nkjørt: operatøren\n")
|
|
row = gate.score_runbook(cfg, tmp_path, now=_NOW)
|
|
assert row.status == gate.NOT_MEASURED
|
|
assert any("dato" in x for x in row.exceptions)
|
|
_attestation_lines(tmp_path, digest)
|
|
row = gate.score_runbook(cfg, tmp_path, now=_NOW)
|
|
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN)
|
|
|
|
|
|
def test_a_duplicated_key_in_the_attestation_is_red(tmp_path: Path) -> None:
|
|
cfg = _CONFIG["runbook"]
|
|
digest = _runbook(tmp_path)
|
|
_attestation_lines(tmp_path, digest)
|
|
path = tmp_path / cfg["attestation"]
|
|
path.write_text(path.read_text(encoding="utf-8") + "kjørt: en annen\n", encoding="utf-8")
|
|
row = gate.score_runbook(cfg, tmp_path, now=_NOW)
|
|
assert row.status == gate.NOT_MEASURED
|
|
assert any("to ganger" in x for x in row.exceptions)
|
|
|
|
|
|
def test_m5_the_gate_never_writes_the_attestation_itself(tmp_path: Path) -> None:
|
|
"""M-5: kan gaten skrive fila den leser, er rad 6 en sløyfe som bekrefter seg selv."""
|
|
_write(tmp_path / _CONFIG["runbook"]["path"], "# kjørebok\n")
|
|
attest = tmp_path / _CONFIG["runbook"]["attestation"]
|
|
rows = gate.evaluate(config=_CONFIG, repo_root=tmp_path, src=_SRC, probe_runner=_probes())
|
|
assert not attest.exists(), "gaten skrev operatørens attestering — da beviser rad 6 ingenting"
|
|
assert _row(rows, "kjørebok").status == gate.NOT_MEASURED
|
|
assert any("aldri" in a for a in _row(rows, "kjørebok").attests)
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Hele gaten
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_gate_is_red_today_and_every_row_is_red_for_a_measured_reason() -> None:
|
|
rows = gate.evaluate(config=_CONFIG, repo_root=_REPO, src=_SRC, probe_runner=_probes())
|
|
assert gate.exit_code(rows) == 1
|
|
assert [r.key for r in rows] == [
|
|
"verktøykasse",
|
|
"roller",
|
|
"ingen-claude-vei",
|
|
"ingen-modellkall",
|
|
"foundry",
|
|
"kjørebok",
|
|
]
|
|
for row in rows:
|
|
assert row.status in {gate.GREEN, gate.RED, gate.NOT_MEASURED}
|
|
if row.status != gate.GREEN:
|
|
assert row.k is not None and row.n is not None, f"{row.key} mangler k av N"
|
|
assert row.exceptions, f"{row.key} er ikke grønn uten å si hvorfor"
|
|
|
|
|
|
def test_the_probe_runner_separates_a_test_that_exists_from_one_that_does_not() -> None:
|
|
"""Kjent-positiv for selve probe-mekanismen: «missing» over hele lista ville vært et
|
|
måleresultat uten nevner. Nevneren er to nodeid-er — én som finnes i suiten og én som ikke
|
|
gjør det — og mekanismen må skille dem. Proben peker med vilje UT av denne fila: en nodeid
|
|
herfra ville fått barne-pytest til å kjøre denne testen igjen, i det uendelige."""
|
|
lever = "tests/test_ledger.py::test_totals_per_project_and_portfolio"
|
|
dod = "tests/test_ledger.py::test_finnes_ikke_xyzzy"
|
|
outcomes = v1_gate.run_probes([lever, dod], _REPO)
|
|
assert outcomes[lever] == "passed"
|
|
assert outcomes[dod] == "missing"
|
|
|
|
|
|
def test_render_names_every_row_the_budget_choice_and_the_runbook_rule() -> None:
|
|
rows = gate.evaluate(config=_CONFIG, repo_root=_REPO, src=_SRC, probe_runner=_probes())
|
|
text = gate.render(rows)
|
|
for row in rows:
|
|
assert row.title in text
|
|
assert gate.BUDGET_IN_B in text
|
|
assert gate.RUNBOOK_RULE in text
|
|
|
|
|
|
def test_the_command_runs_offline_and_exits_one() -> None:
|
|
proc = subprocess.run(
|
|
[sys.executable, "-m", "portfolio_optimiser.evals.b_gate", "--json"],
|
|
cwd=_REPO,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert proc.returncode == 1, proc.stderr
|
|
payload = json.loads(proc.stdout)
|
|
assert payload["exit"] == 1
|
|
assert len(payload["rows"]) == 6
|
|
|
|
|
|
def test_a_bad_attestation_path_is_usage_error_not_a_silent_green(tmp_path: Path) -> None:
|
|
proc = subprocess.run(
|
|
[sys.executable, "-m", "portfolio_optimiser.evals.b_gate", "--attest", str(tmp_path / "x")],
|
|
cwd=_REPO,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert proc.returncode == 2
|
|
|
|
|
|
# =============================================================================================
|
|
# REPARASJONEN 19.09.2026 (PM-sjekkpunktet på 207337c): nevnerne utledet av KILDEN, en dør som
|
|
# må være REGISTRERT og bevist med atferd, hele den publiserte flaten, og en attestering som kan
|
|
# felles. Hver arm under feller på en ASSERT om atferd, aldri ved innsamling.
|
|
# =============================================================================================
|
|
|
|
#: po-funksjoner ``run.py::run_project`` faktisk kaller — talt av armens EGEN parser under.
|
|
#: 41 = PMs 39 deterministiske + de to som krever en chatklient og holdes utenfor med grunn.
|
|
_RUN_PATH_CALLS_TODAY = 41
|
|
#: utboks-skrivere: definert i outbox.py / kalt i kjørestien. PMs tall, talt om igjen her.
|
|
_WRITERS_DEFINED = 10
|
|
_WRITERS_IN_RUN_PATH = 7
|
|
#: publiserte filer i git-manifestet (uttrekk og arbeidstre gir SAMME tall — det var hele poenget
|
|
#: med å slutte å telle filtreet: 435 i arbeidstreet var to gitignorerte .local.md-filer).
|
|
_PUBLISHED_TODAY = 512
|
|
_UNDECODABLE_TODAY = 1
|
|
|
|
|
|
def _manifest() -> dict[str, Any]:
|
|
manifest = _CONFIG["no_claude_path"].get("manifest")
|
|
assert manifest is not None, "kontrakten utleder ikke flaten av repo-manifestet"
|
|
return dict(manifest)
|
|
|
|
|
|
def _run_path_spec() -> dict[str, Any]:
|
|
spec = _CONFIG.get("run_path")
|
|
assert spec is not None, "kontrakten navngir ingen kjørested — nevneren har da ingen kilde"
|
|
return dict(spec)
|
|
|
|
|
|
def _independently_counted_run_path() -> dict[str, str]:
|
|
"""po-funksjonene ``run_project`` kaller, talt av TESTENS egen parser — ingen gate-kode rørt.
|
|
|
|
Regelen: et navn importert fra en ``portfolio_optimiser``-undermodul og kalt i scopet, eller
|
|
et attributt på en modul importert som ``from portfolio_optimiser import X``. Klasser og
|
|
unntak (stor forbokstav) er ikke steg; private hjelpere i run.py heller ikke."""
|
|
tree = ast.parse((_SRC / "run.py").read_text(encoding="utf-8"))
|
|
symbols: dict[str, str] = {}
|
|
aliases: set[str] = set()
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.ImportFrom):
|
|
continue
|
|
module = node.module or ""
|
|
if module == "portfolio_optimiser":
|
|
aliases |= {a.asname or a.name for a in node.names}
|
|
elif module.startswith("portfolio_optimiser."):
|
|
for alias in node.names:
|
|
symbols[alias.asname or alias.name] = module.split(".")[-1] + ".py"
|
|
scope = next(
|
|
n
|
|
for n in ast.walk(tree)
|
|
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == "run_project"
|
|
)
|
|
found: dict[str, str] = {}
|
|
for call in ast.walk(scope):
|
|
if not isinstance(call, ast.Call):
|
|
continue
|
|
func = call.func
|
|
if isinstance(func, ast.Name) and func.id in symbols and func.id[:1].islower():
|
|
found[func.id] = symbols[func.id]
|
|
elif (
|
|
isinstance(func, ast.Attribute)
|
|
and isinstance(func.value, ast.Name)
|
|
and func.value.id in aliases
|
|
and func.attr[:1].islower()
|
|
):
|
|
found[func.attr] = f"{func.value.id}.py"
|
|
return found
|
|
|
|
|
|
def test_the_run_path_denominator_is_read_from_the_source_not_from_the_contract() -> None:
|
|
"""Rad 1s nevner kom fra gatens EGEN b_gate.json. Nå leses kjørestien av kilden, og testen
|
|
teller den om igjen selv."""
|
|
mine = _independently_counted_run_path()
|
|
assert len(mine) == _RUN_PATH_CALLS_TODAY, sorted(mine)
|
|
theirs = gate.run_path_calls(_SRC, _run_path_spec())
|
|
assert set(theirs) == set(mine), sorted(set(mine) ^ set(theirs))
|
|
|
|
|
|
def test_every_call_in_the_run_path_is_either_a_declared_step_or_held_out_with_a_reason() -> None:
|
|
"""Den ene egenskapen en kuratert liste ikke har: ingenting kan utelates STILLE."""
|
|
calls = set(_independently_counted_run_path())
|
|
declared = {str(s["symbol"]) for s in _CONFIG["steps"]}
|
|
held = {
|
|
str(h["symbol"]): str(h.get("reason", "")) for h in _run_path_spec().get("held_out", ())
|
|
}
|
|
assert not (calls - declared - set(held)), sorted(calls - declared - set(held))
|
|
assert all(held.values()), [s for s, why in held.items() if not why]
|
|
assert len(calls & declared) + len(held) == _RUN_PATH_CALLS_TODAY
|
|
|
|
|
|
def test_the_outbox_writers_the_run_path_uses_are_all_declared_steps() -> None:
|
|
"""PMs 7 av 10, talt om igjen her: artefaktfamilien er ikke tre filer, den er sju."""
|
|
outbox_tree = ast.parse((_SRC / "outbox.py").read_text(encoding="utf-8"))
|
|
writers = {
|
|
n.name
|
|
for n in outbox_tree.body
|
|
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name.startswith("write_")
|
|
}
|
|
assert len(writers) == _WRITERS_DEFINED
|
|
in_path = writers & set(_independently_counted_run_path())
|
|
assert len(in_path) == _WRITERS_IN_RUN_PATH, sorted(in_path)
|
|
declared = {str(s["symbol"]) for s in _CONFIG["steps"]}
|
|
assert not (in_path - declared), sorted(in_path - declared)
|
|
|
|
|
|
def _mini_run_path(root: Path, *, extra_call: bool) -> tuple[Path, dict[str, Any]]:
|
|
"""En konstruert kilde med en kjøresti som kaller ett eller to po-steg."""
|
|
src = root / "minisrc"
|
|
_write(src / "steg.py", "def alfa():\n return 1\n\n\ndef beta():\n return 2\n")
|
|
body = " alfa()\n" + (" beta()\n" if extra_call else "")
|
|
_write(
|
|
src / "kjor.py",
|
|
f"from minisrc.steg import alfa, beta\n\n\nasync def run_project():\n{body} return 0\n",
|
|
)
|
|
return src, {"module": "kjor.py", "scope": "run_project", "package": "minisrc"}
|
|
|
|
|
|
def test_an_undeclared_call_in_the_run_path_raises_the_denominator_and_is_named(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""M-6: nevneren kan ikke krympe stille — et kall ingen erklærte teller som et steg UTEN dør."""
|
|
src, spec = _mini_run_path(tmp_path, extra_call=True)
|
|
steps = [
|
|
{
|
|
"id": "alfa",
|
|
"label": "alfa",
|
|
"module": "steg.py",
|
|
"symbol": "alfa",
|
|
"driver": {"module": "kjor.py", "scope": "run_project"},
|
|
"entry": {"kind": "module-main", "module": "kjor.py", "scope": "main"},
|
|
}
|
|
]
|
|
row = gate.score_toolbox(steps, src, spec, {}, tmp_path)
|
|
assert row.n == 2, "beta kalles i kjørestien og er verken erklært eller holdt utenfor"
|
|
assert any("beta" in x for x in row.exceptions)
|
|
|
|
|
|
def test_a_held_out_call_leaves_the_denominator_but_is_named_with_its_reason(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
src, spec = _mini_run_path(tmp_path, extra_call=True)
|
|
spec["held_out"] = [{"symbol": "beta", "reason": "hjelper inne i alfa, ikke et eget steg"}]
|
|
steps = [
|
|
{
|
|
"id": "alfa",
|
|
"label": "alfa",
|
|
"module": "steg.py",
|
|
"symbol": "alfa",
|
|
"driver": {"module": "kjor.py", "scope": "run_project"},
|
|
"entry": {"kind": "module-main", "module": "kjor.py", "scope": "main"},
|
|
}
|
|
]
|
|
row = gate.score_toolbox(steps, src, spec, {}, tmp_path)
|
|
assert row.n == 1
|
|
assert any("beta" in d and "hjelper inne i alfa" in d for d in row.diagnostics), row.diagnostics
|
|
|
|
|
|
def test_removing_a_declared_step_does_not_shrink_the_denominator(tmp_path: Path) -> None:
|
|
"""A2 fra sjekkpunktet: «utboks» fjernet fra steps ga «3 av 12» uten en klage."""
|
|
src, spec = _mini_run_path(tmp_path, extra_call=True)
|
|
row = gate.score_toolbox([], src, spec, {}, tmp_path)
|
|
assert row.n == 2, "begge kallene står igjen i kjørestien selv om kontrakten glemte dem"
|
|
assert len(row.exceptions) == 2
|
|
|
|
|
|
# --- døren: registrert, av en art gaten kan verifisere, og bevist med atferd -------------------
|
|
|
|
|
|
def _fake_pyproject(root: Path, scripts: dict[str, str]) -> None:
|
|
lines = "\n".join(f'{name} = "{target}"' for name, target in scripts.items())
|
|
_write(
|
|
root / "pyproject.toml",
|
|
f'[project]\nname = "fake"\nversion = "0"\n\n[project.scripts]\n{lines}\n\n[tool.x]\ny = 1\n',
|
|
)
|
|
|
|
|
|
def test_an_entry_kind_the_gate_cannot_verify_is_never_a_door(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
ok, why = gate.registered_entry(
|
|
src, tmp_path, "fakesrc", {"kind": "magi", "module": "driver.py", "scope": "main"}
|
|
)
|
|
assert ok is False and "magi" in why
|
|
assert "magi" not in gate.ENTRY_KINDS
|
|
|
|
|
|
def test_a_console_script_is_a_door_only_when_the_manifest_registers_it(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
entry = {"kind": "console-script", "module": "driver.py", "scope": "main"}
|
|
_fake_pyproject(tmp_path, {})
|
|
ok, why = gate.registered_entry(src, tmp_path, "fakesrc", entry)
|
|
assert ok is False and "pyproject" in why
|
|
_fake_pyproject(tmp_path, {"fake-kommando": "fakesrc.driver:main"})
|
|
assert gate.registered_entry(src, tmp_path, "fakesrc", entry) == (True, "")
|
|
|
|
|
|
def test_a_module_main_is_a_door_only_with_the_dunder_guard(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
entry = {"kind": "module-main", "module": "driver.py", "scope": "main"}
|
|
assert gate.registered_entry(src, tmp_path, "fakesrc", entry) == (True, "")
|
|
stripped = (src / "driver.py").read_text(encoding="utf-8")
|
|
_write(src / "driver.py", stripped[: stripped.index("if __name__")])
|
|
ok, why = gate.registered_entry(src, tmp_path, "fakesrc", entry)
|
|
assert ok is False and "__main__" in why
|
|
|
|
|
|
def test_a_subcommand_is_a_door_only_when_the_parser_registers_that_name(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
entry = {
|
|
"kind": "subcommand",
|
|
"module": "driver.py",
|
|
"scope": "main",
|
|
"command": "naviger-pakke",
|
|
}
|
|
assert gate.registered_entry(src, tmp_path, "fakesrc", entry)[0] is False
|
|
_write(
|
|
src / "driver.py",
|
|
(src / "driver.py").read_text(encoding="utf-8")
|
|
+ '\n\ndef _parser(sub):\n sub.add_parser("naviger-pakke")\n',
|
|
)
|
|
assert gate.registered_entry(src, tmp_path, "fakesrc", entry) == (True, "")
|
|
|
|
|
|
def test_a_stub_main_is_not_a_door_without_a_registered_entry(tmp_path: Path) -> None:
|
|
"""A1 fra sjekkpunktet, felt: en modul med bare `def main(): return <symbol>()` tok rad 1 fra
|
|
3 til 4 av 13. Navn alene er aldri grønt."""
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
_fake_pyproject(tmp_path, {})
|
|
steps = _one_step()
|
|
steps[0]["entry"] = {"kind": "console-script", "module": "driver.py", "scope": "main"}
|
|
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
|
|
assert (row.k, row.n) == (0, 1)
|
|
assert any("pyproject" in x for x in row.exceptions), row.exceptions
|
|
|
|
|
|
def test_a_registered_door_without_a_passing_behaviour_probe_does_not_count(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""«Kallet med en fixture gir det dokumenterte artefaktet» — et navn er ikke en atferd."""
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
steps = _one_step()
|
|
row = gate.score_toolbox(steps, src, None, {}, tmp_path)
|
|
assert (row.k, row.n) == (0, 1)
|
|
assert any("atferdsprobe" in x for x in row.exceptions), row.exceptions
|
|
row = gate.score_toolbox(steps, src, None, {_FAKE_PROBE: "skipped"}, tmp_path)
|
|
assert row.k == 0, "hoppet over er ikke bestått"
|
|
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
|
|
assert (row.k, row.n, row.status) == (1, 1, gate.GREEN)
|
|
|
|
|
|
def test_a_step_without_any_probe_is_never_a_door(tmp_path: Path) -> None:
|
|
src = _fake_src(tmp_path, chat_client=False)
|
|
steps = _one_step()
|
|
steps[0].pop("probe")
|
|
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
|
|
assert (row.k, row.n) == (0, 1)
|
|
assert any("ingen atferdsprobe" in x for x in row.exceptions), row.exceptions
|
|
|
|
|
|
def test_the_door_rule_claims_only_kinds_the_gate_can_verify() -> None:
|
|
"""A8: EXTERNAL_DOOR påsto at en MCP-registrert dør telles; «kind» ble aldri lest."""
|
|
assert gate.ENTRY_KINDS
|
|
assert "mcp" not in gate.EXTERNAL_DOOR.casefold().replace("mcp ville", "")
|
|
assert all(kind in gate.EXTERNAL_DOOR for kind in gate.ENTRY_KINDS)
|
|
assert '"kind"' in Path(gate.__file__).read_text(encoding="utf-8")
|
|
|
|
|
|
# --- rad 3: hele den publiserte flaten, og de indirekte veiene ---------------------------------
|
|
|
|
|
|
def test_the_published_surface_is_the_repo_manifest_and_not_a_handlist() -> None:
|
|
"""A10: main.py, examples/, spikes/ og contexts/ er publisert, men lå utenfor roots.
|
|
|
|
Armen holder i BEGGE verdener — arbeidstreet (git-manifestet) og et rent uttrekk (filtreet),
|
|
som er nøyaktig det publiserte. Den nøyaktige nevneren pinnes av armen under, som er den ene
|
|
som trenger repoets egen git-metadata."""
|
|
files, _source = gate.published_files(_REPO, _manifest())
|
|
names = {str(p.relative_to(_REPO)) for p in files}
|
|
assert "main.py" in names and "CLAUDE.md" in names and "llms.txt" in names
|
|
assert any(n.startswith("examples/") for n in names)
|
|
assert any(n.startswith("spikes/") for n in names)
|
|
assert any(n.startswith("contexts/") for n in names)
|
|
assert len(files) > 400, len(files)
|
|
|
|
|
|
def test_the_surface_count_matches_an_independent_count_of_the_manifest() -> None:
|
|
"""Den ENE armen som trenger repoets egen git-metadata: i et rent uttrekk («not a git
|
|
repository») finnes ikke manifestet å telle mot, og armen er da et uttrekksartefakt — samme
|
|
klasse som de tre som allerede er navngitt. Nevneren pinnes HER, ikke i armen over."""
|
|
tracked = subprocess.run(
|
|
["git", "ls-files", "-z"], cwd=_REPO, capture_output=True, text=True, check=True
|
|
).stdout.split("\0")
|
|
mine = {t for t in tracked if t}
|
|
assert len(mine) == _PUBLISHED_TODAY, len(mine)
|
|
files, source = gate.published_files(_REPO, _manifest())
|
|
assert {str(p.relative_to(_REPO)) for p in files} == mine
|
|
assert "git-manifestet" == source
|
|
|
|
|
|
def test_a_surface_without_its_sentinel_is_not_measured_instead_of_green(tmp_path: Path) -> None:
|
|
"""A4: «6 av 6 GRØNN over 0 publiserte filer». Et fravær uten nevner er ikke et bevis."""
|
|
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
|
|
cfg.pop("roots", None)
|
|
cfg["manifest"] = {"sentinel": "src/portfolio_optimiser/run.py"}
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert row.status == gate.NOT_MEASURED, row.reason
|
|
assert any("sentinel" in x for x in row.exceptions), row.exceptions
|
|
assert "0 publiserte filer" in row.reason
|
|
|
|
|
|
def test_the_row_names_which_manifest_it_read() -> None:
|
|
"""Nevneren står i radens EGEN tekst, sammen med hvor lista kom fra og hvor mange filer som
|
|
ikke lot seg avkode — «0 treff» uten nevner er ikke et bevis."""
|
|
row = gate.score_no_claude_path(_CONFIG["no_claude_path"], _REPO)
|
|
files, source = gate.published_files(_REPO, _manifest())
|
|
assert f"over {len(files)} publiserte filer" in row.reason
|
|
assert source in row.reason
|
|
assert f"{_UNDECODABLE_TODAY} ulesbare" in row.reason
|
|
|
|
|
|
def test_the_surface_falls_back_to_the_tree_when_there_is_no_git_metadata(tmp_path: Path) -> None:
|
|
"""Et rent uttrekk (`git archive | tar -x`) har ingen .git — og ER nøyaktig det publiserte."""
|
|
_write(tmp_path / "src" / "portfolio_optimiser" / "run.py", "x = 1\n")
|
|
_write(tmp_path / "main.py", "y = 2\n")
|
|
_write(tmp_path / "__pycache__" / "skrot.pyc", "nei\n")
|
|
files, source = gate.published_files(tmp_path, {"sentinel": "src/portfolio_optimiser/run.py"})
|
|
names = {str(p.relative_to(tmp_path)) for p in files}
|
|
assert names == {"src/portfolio_optimiser/run.py", "main.py"}
|
|
assert "uttrekk" in source
|
|
|
|
|
|
#: PMs fem realistiske kall-varianter, base64-kodet av samme grunn som mønstrene i kontrakten:
|
|
#: tests/ er selv en del av den publiserte flaten raden skanner, og en klartekst-variant her
|
|
#: ville registrert seg som sitt eget funn.
|
|
_INDIRECT_VARIANTS = (
|
|
("direkte", "c3VicHJvY2Vzcy5ydW4oWyJjbGF1ZGUiLCAiLXAiLCAieCJdKQ=="),
|
|
("absolutt-sti", "c3VicHJvY2Vzcy5ydW4oWyIvdXNyL2xvY2FsL2Jpbi9jbGF1ZGUiLCAiLXAiLCAieCJdKQ=="),
|
|
("liste-i-variabel", "Y21kID0gWyJjbGF1ZGUiLCAiLXAiLCAieCJdCnN1YnByb2Nlc3MucnVuKGNtZCk="),
|
|
(
|
|
"konstant",
|
|
"Q0xBVURFX0JJTiA9ICJjbGF1ZGUiCnN1YnByb2Nlc3MucnVuKFtDTEFVREVfQklOLCAiLXAiLCAieCJdKQ==",
|
|
),
|
|
("shell-streng", "c3VicHJvY2Vzcy5ydW4oc2hsZXguc3BsaXQoImNsYXVkZSAtcCB4Iikp"),
|
|
)
|
|
|
|
|
|
def test_every_indirect_way_of_starting_claude_is_caught(tmp_path: Path) -> None:
|
|
"""A9: 4 av 5 realistiske varianter slapp forbi. Hver plantes som kjent-positiv, én om
|
|
gangen, og hver skal felle raden."""
|
|
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
|
|
cfg.pop("roots", None)
|
|
cfg["manifest"] = {"sentinel": "src/run.py"}
|
|
_write(tmp_path / "src" / "run.py", "x = 1\n")
|
|
assert gate.score_no_claude_path(cfg, tmp_path).status == gate.GREEN, "kontroll: ren flate"
|
|
for name, snippet in _INDIRECT_VARIANTS:
|
|
planted = tmp_path / "src" / f"{name}.py"
|
|
_write(planted, gate.decode(snippet) + "\n")
|
|
row = gate.score_no_claude_path(cfg, tmp_path)
|
|
assert row.status == gate.RED, name
|
|
assert any(name in x for x in row.exceptions), (name, row.exceptions)
|
|
planted.unlink()
|
|
assert gate.score_no_claude_path(cfg, tmp_path).status == gate.GREEN
|
|
|
|
|
|
def test_the_row_states_the_limit_its_patterns_cannot_reach() -> None:
|
|
row = gate.score_no_claude_path(_CONFIG["no_claude_path"], _REPO)
|
|
assert any("satt sammen" in a or "miljøvariabel" in a for a in row.attests), row.attests
|
|
|
|
|
|
# --- rad 4, 5 og 6: hver nevner med en navngitt kilde -----------------------------------------
|
|
|
|
|
|
def test_row4_counts_only_checks_whose_named_source_resolves(tmp_path: Path) -> None:
|
|
"""M=3 var en konstant i gatens egen konfig. Nå er hver sjekk bundet til et symbol i kilden."""
|
|
cfg = json.loads(json.dumps(_CONFIG["no_model_calls"]))
|
|
row = gate.score_no_model_calls(cfg, {}, _SRC)
|
|
assert (row.k, row.n, row.status) == (0, 3, gate.RED)
|
|
for name, check in cfg["checks"].items():
|
|
assert isinstance(check, dict) and "source" in check, name
|
|
tree = ast.parse((_SRC / check["source"]["module"]).read_text(encoding="utf-8"))
|
|
names = {
|
|
n.name for n in tree.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
} | {
|
|
t.id
|
|
for n in tree.body
|
|
if isinstance(n, ast.Assign)
|
|
for t in n.targets
|
|
if isinstance(t, ast.Name)
|
|
}
|
|
assert check["source"]["symbol"] in names, name
|
|
src = tmp_path / "tom"
|
|
src.mkdir()
|
|
row = gate.score_no_model_calls(cfg, {}, src)
|
|
assert row.n == 0, "en sjekk uten kilde i koden teller ikke med i nevneren"
|
|
assert len(row.exceptions) == 3
|
|
|
|
|
|
def test_row5_counts_each_structural_requirement_on_its_own(tmp_path: Path) -> None:
|
|
"""M=2 blandet profil, fabrikk og injeksjonssøm til ÉN enhet. Nå telles hvert krav for seg,
|
|
og hvert av dem er et symbol i kilden."""
|
|
row = gate.score_foundry(_CONFIG["foundry"], {}, _SRC)
|
|
members = len(_CONFIG["foundry"]["profile"]["members"])
|
|
evidence = len(_CONFIG["foundry"]["evidence"])
|
|
assert row.n == members + 2 + evidence
|
|
assert (row.k, row.status) == (members + 2, gate.RED)
|
|
nodeid = _CONFIG["foundry"]["evidence"]["samme-skjema"][0]
|
|
row = gate.score_foundry(_CONFIG["foundry"], {nodeid: "passed"}, _SRC)
|
|
assert row.status == gate.GREEN
|
|
|
|
|
|
def test_row5_denominator_follows_the_named_members(tmp_path: Path) -> None:
|
|
cfg = json.loads(json.dumps(_CONFIG["foundry"]))
|
|
cfg["profile"]["members"].append("FINNES_IKKE")
|
|
row = gate.score_foundry(cfg, {}, _SRC)
|
|
assert row.n == len(cfg["profile"]["members"]) + 2 + len(cfg["evidence"])
|
|
assert any("FINNES_IKKE" in x for x in row.exceptions)
|
|
|
|
|
|
def test_row6_denominator_comes_from_the_artefacts_the_contract_names(tmp_path: Path) -> None:
|
|
artefacts = _CONFIG["runbook"].get("artefacts")
|
|
assert artefacts is not None, "M=2 var en konstant i koden, ikke noe kontrakten navngir"
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path)
|
|
assert row.n == len(artefacts)
|
|
|
|
|
|
# --- rad 6: attesteringen kan felles ----------------------------------------------------------
|
|
|
|
|
|
def _runbook(root: Path) -> str:
|
|
text = "# kjørebok\n\nsteg 1: naviger pakken\n"
|
|
_write(root / _CONFIG["runbook"]["path"], text)
|
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _attestation_lines(root: Path, digest: str, **over: str) -> None:
|
|
fields = {
|
|
"kjørebok": _CONFIG["runbook"]["path"],
|
|
"sjekksum": digest,
|
|
"kjørt": "operatøren",
|
|
"dato": "2026-09-19",
|
|
}
|
|
fields.update(over)
|
|
_write(
|
|
root / _CONFIG["runbook"]["attestation"],
|
|
"".join(f"{k}: {v}\n" for k, v in fields.items()),
|
|
)
|
|
|
|
|
|
def test_a_valid_attestation_is_the_only_thing_that_turns_row6_green(tmp_path: Path) -> None:
|
|
"""rc-0-kontrollen FØRST: en gate som nekter alt er like ubrukelig som en som nekter noe."""
|
|
digest = _runbook(tmp_path)
|
|
_attestation_lines(tmp_path, digest)
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
|
|
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN), row.exceptions
|
|
|
|
|
|
def test_an_attestation_that_names_another_runbook_is_refused(tmp_path: Path) -> None:
|
|
"""A7: «kjørebok: x» ga 2 av 2 GRØNN."""
|
|
digest = _runbook(tmp_path)
|
|
_attestation_lines(tmp_path, digest, **{"kjørebok": "x"})
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
|
|
assert row.status == gate.NOT_MEASURED
|
|
assert any("kjørebok" in x for x in row.exceptions)
|
|
|
|
|
|
def test_an_attestation_whose_checksum_does_not_match_the_runbook_is_refused(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""En attestering av en ANNEN versjon av kjøreboka er ikke en attestering av denne."""
|
|
_runbook(tmp_path)
|
|
_attestation_lines(tmp_path, "0" * 64)
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
|
|
assert row.status == gate.NOT_MEASURED
|
|
assert any("sjekksum" in x for x in row.exceptions)
|
|
|
|
|
|
def test_an_attestation_dated_in_the_future_is_refused(tmp_path: Path) -> None:
|
|
"""A7: framtidsdato 3026-01-01 ga GRØNN. Samme regel som v1-gaten, gjenbrukt."""
|
|
digest = _runbook(tmp_path)
|
|
_attestation_lines(tmp_path, digest, dato="3026-01-01")
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
|
|
assert row.status == gate.NOT_MEASURED
|
|
assert any("framtiden" in x for x in row.exceptions)
|
|
|
|
|
|
def test_an_attestation_without_a_real_date_is_refused(tmp_path: Path) -> None:
|
|
digest = _runbook(tmp_path)
|
|
_attestation_lines(tmp_path, digest, dato="x")
|
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
|
|
assert row.status == gate.NOT_MEASURED
|
|
assert any("ISO" in x or "dato" in x for x in row.exceptions)
|
|
|
|
|
|
def test_an_attestation_with_a_bom_is_still_read(tmp_path: Path) -> None:
|
|
"""v1-gaten TÅLER BOM — en editor som skriver den er ikke operatørens feil."""
|
|
digest = _runbook(tmp_path)
|
|
_attestation_lines(tmp_path, digest)
|
|
path = tmp_path / _CONFIG["runbook"]["attestation"]
|
|
path.write_bytes(b"\xef\xbb\xbf" + path.read_bytes())
|
|
assert gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW).status == gate.GREEN
|