fix: findings from the independent review

Scenario runner: stops when overridden base parameters leave the start out
of balance (new field startlikevekt: krev, kalibrer, ubalanse); stops on
non-finite results; says that models with a court queue drift slowly even
without a shock and prints the drift before the shock; queue in months of
inflow; never deletes files it did not write; stricter limits.
Check: anchored on the note's own checksums for core and reference files,
runs the scenario files too, exact comparison for the diagnosis, honest
wording when parts were skipped or an extension is broken.
Launcher: unknown field names in input are rejected with a suggestion;
unwritable folders give a message, not a traceback.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-11 15:27:14 +01:00
commit 776325c680
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
14 changed files with 1524 additions and 199 deletions

View file

@ -3,16 +3,21 @@
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any
import kjor as kjor_modul
import pytest
from conftest import DOMSTOL, EKSEMPLER, FASIT, MODELL, NORGE, ROT, fasit, kjor, lag_venv_omvei
if TYPE_CHECKING:
from collections.abc import Callable
UTEN_SITE = [sys.executable, "-S"] # samme Python uten site-packages: numpy og scipy mangler
@ -119,6 +124,74 @@ def test_ingen_pycache_i_kjernen(kopi: Path) -> None:
assert not (kopi / "modell" / "kjerne" / "__pycache__").exists()
# ---------------- ukjente feltnavn (funn 5) ----------------
def skriv_variant(tmp_path: Path, endre: Callable[[dict[str, Any]], None], navn: str = "variant.json") -> Path:
"""En kopi av eksempel_domstol.json med en endring."""
data = json.loads(DOMSTOL.read_text(encoding="utf-8"))
endre(data)
fil = tmp_path / navn
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return fil
def skrivefeil_i_ledd(data: dict[str, Any]) -> None:
domstol = data["ledd"][2]
domstol["prognose_innstrømning"] = domstol.pop("prognose_innstromning")
@pytest.mark.parametrize("kommando", ["diagnose", "forlop"])
def test_skrivefeil_i_ledd_avvises(kommando: str, tmp_path: Path) -> None:
fil = skriv_variant(tmp_path, skrivefeil_i_ledd)
prosess = kjor(kommando, str(fil))
assert prosess.returncode == 2
assert prosess.stdout == ""
assert prosess.stderr.startswith(
'Feil i inndata, ledd 3 (Domstol): feltet "prognose_innstrømning" finnes ikke. '
'Mente du "prognose_innstromning"? Gyldige felt: navn, innstromning, kapasitet, restanse, maal_tid,')
def test_skrivefeil_i_styring_avvises(tmp_path: Path) -> None:
def endre(data: dict[str, Any]) -> None:
data["styring"]["reaktivitett"] = data["styring"].pop("reaktivitet")
prosess = kjor("forlop", str(skriv_variant(tmp_path, endre)))
assert prosess.returncode == 2
assert prosess.stderr == ('Feil i inndata, styring: feltet "reaktivitett" finnes ikke. Mente du "reaktivitet"? '
"Gyldige felt: forsinkelse, reaktivitet, merknad, kilde, kilder.\n")
def test_merknadsfelt_og_ukjente_toppfelt_er_tillatt(tmp_path: Path) -> None:
def endre(data: dict[str, Any]) -> None:
data["ledd"][2].update(merknad="fri tekst", kilde="SSB", kilder=["a", "b"])
data["styring"]["merknad"] = "regime A"
data["noe_annet"] = {"fritt": True}
prosess = kjor("diagnose", str(skriv_variant(tmp_path, endre)))
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == fasit("diagnose_eksempel_domstol.txt")
def test_feltene_leses_fra_kjernen_i_modellen(kopi: Path, tmp_path: Path) -> None:
diagnose = kopi / "modell" / "kjerne" / "diagnose.py"
tekst = diagnose.read_text(encoding="utf-8")
assert '"innstromning_for_okning")' in tekst
diagnose.write_text(tekst.replace('"innstromning_for_okning")', '"innstromning_for_okning", "nytt_felt")', 1),
encoding="utf-8")
fil = skriv_variant(tmp_path, lambda data: data["ledd"][0].update(nytt_felt=1))
assert kjor("diagnose", str(fil)).returncode == 2
prosess = kjor("--modell", str(kopi), "diagnose", str(fil))
assert prosess.returncode == 0, prosess.stderr
def test_feil_form_overlates_til_kjernen(tmp_path: Path) -> None:
fil = tmp_path / "feil.json"
fil.write_text('{"ledd": [1, {"navn": "A"}]}', encoding="utf-8")
prosess = kjor("diagnose", str(fil))
assert prosess.returncode == 2
assert prosess.stderr == "Feil i inndata, ledd 1: hvert ledd må være et objekt med felt og verdier.\n"
# ---------------- --data og valg av Python ----------------
@pytest.mark.parametrize("verdi", [None, "", " ", "${CLAUDE_PLUGIN_DATA}", "/a/${CLAUDE_PLUGIN_DATA}/b"])
@ -229,6 +302,18 @@ def test_velg_tolk(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
assert kjor_modul.velg_tolk(tmp_path).hvor == "mangler"
@pytest.mark.parametrize(("system", "start", "sitert"), [
("nt", "python ", '"/data med mellomrom"'), ("posix", "python3 ", "'/data med mellomrom'"),
])
def test_foreslatt_kommando_per_system(system: str, start: str, sitert: str) -> None:
valg = kjor_modul.Valg(argv=[], data=Path("/data med mellomrom"), data_oppgitt=True, modell=MODELL,
kommando="forlop", args=[])
tekst = kjor_modul.kommando_tekst(valg, "miljo", "--opprett", system=system)
assert tekst.startswith(start)
assert f"--data {sitert} miljo --opprett" in tekst
assert kjor_modul.python_navn(system) == start.strip()
# ---------------- miljo --opprett (uten å installere) ----------------
def test_opprett_proever_laaste_versjoner_forst(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@ -302,6 +387,7 @@ def test_modell_i_arbeidskopi(kopi: Path, undermappe: str) -> None:
@pytest.mark.parametrize("kommando", ["scenario", "arbeidskopi"])
def test_modul_som_ikke_finnes_ennaa(kopi: Path, kommando: str) -> None:
(kopi / "modell" / f"{kommando}.py").unlink()
prosess = kjor("--modell", str(kopi), kommando, "x")
assert prosess.returncode == 2
assert f"Kommandoen «{kommando}» er ikke på plass i denne modellen ennå" in prosess.stderr
@ -359,6 +445,29 @@ def test_notatet_nekter_ut_mappe_i_modellen() -> None:
assert not (MODELL / "kjerne" / "data").exists()
@pytest.mark.skipif(os.name == "nt" or os.geteuid() == 0, reason="skrivebeskyttelse virker ikke for root")
def test_notatet_i_mappe_som_ikke_kan_skrives(tmp_path: Path) -> None:
sperret = tmp_path / "sperret"
sperret.mkdir()
sperret.chmod(0o555)
try:
prosess = kjor("notatet", "--ut", str(sperret / "ut"))
finally:
sperret.chmod(0o755)
assert prosess.returncode == 2
assert prosess.stderr.startswith(f"Kan ikke skrive i {(sperret / 'ut').resolve()}: ")
assert prosess.stderr.endswith(". Velg en annen mappe med --ut.\n")
assert "Traceback" not in prosess.stderr
def test_notatet_naar_sim_stopper(kopi: Path, tmp_path: Path) -> None:
(kopi / "modell" / "kjerne" / "sim.py").write_text("print('start')\nraise SystemExit(5)\n", encoding="utf-8")
prosess = kjor("--modell", str(kopi), "notatet", "--ut", str(tmp_path / "ut"))
assert prosess.returncode == 2
assert "Kjøringen av sim.py stoppet underveis (exit-kode 5)" in prosess.stderr
assert "start\n" in prosess.stdout
@pytest.mark.treg
def test_notatet_gjenskaper_datafilene(tmp_path: Path) -> None:
ut = tmp_path / "ut"