test(toolbox): a run id that carries a path must be refused, and write nothing
RED. Seven outbox doors compose `<out_dir>/<run_id>-<artefact>.json` and never look at the run id, so `--outbox-dir <d>/inni --run-id ../../ESCAPE` writes two levels ABOVE the directory the caller named and answers 0. The guard stood on one half of the path and was missing on the other. Reproduced on write-prepass before this file existed. The class is measured, not one case: all seven doors against four path-carrying forms -- relative-up, separator, bare `..`, absolute. Each arm asserts a non-zero exit AND that the file tree under tmp_path is byte-unchanged afterwards; the first alone would be satisfied by a door that refused after writing. An rc-0 control per door runs the same argv with a clean run id, so the refusals measure the run id rather than a malformed call. The file also carries the guard the `--stop-reason` rule never had. `required=True` is called load-bearing in the ledger -- "the run finished" and "we never found out" must not be the same value -- but swapping it for `default=""` passed the whole suite. The arm fells that mutant: no flag is a usage error, and the empty string stays a value the caller can supply on purpose. Measured at this commit: 28 failed, 8 passed (the seven controls and the stop-reason arm). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d8541d583f
commit
446adf7f2b
1 changed files with 218 additions and 0 deletions
218
tests/test_toolbox_run_id_containment.py
Normal file
218
tests/test_toolbox_run_id_containment.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
"""`--run-id` navngir en kjøring, ikke en sti — og de sju utboks-dørene må håndheve det.
|
||||
|
||||
Hver skriver bygger filnavnet sitt som ``<utkatalog>/<run_id>-<artefakt>.json``. Utkatalogen er
|
||||
kallerens å navngi (det er en egen regel), men run-id-en gikk urørt inn i den sammensetningen:
|
||||
``--outbox-dir <kat>/inni --run-id ../../ESCAPE`` skrev artefaktet TO nivåer OVER katalogen
|
||||
kalleren navnga, og svarte 0. Vakten sto på den ene halvdelen av stien og manglet på den andre.
|
||||
|
||||
Klassen måles her, ikke ett tilfelle: alle SJU dørene mot fire former for stibærende run-id —
|
||||
relativ oppover, absolutt, med skilletegn, og den bare ``..``. Hver arm asserterer to ting, fordi
|
||||
én av dem alene ville vært tilfreds med feil oppførsel: at exit-koden er ikke-null, OG at
|
||||
filtreet under `tmp_path` er BYTE-uendret etterpå. En dør som avviste med rett kode etter å ha
|
||||
lagt igjen fila ville passert på den første alene.
|
||||
|
||||
rc-0-kontrollen er obligatorisk: samme argv med en ren run-id må svare 0 og skrive. Uten den
|
||||
måler armene ingenting — en dør som avviste ALT ville vært like grønn som den riktige.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[1]
|
||||
_IR = _REPO / "shared" / "examples" / "bygg-energi-mikro" / "validator-input.json"
|
||||
|
||||
#: En run-id uten stibærende tegn. Den ENE verdien rc-0-kontrollen står på.
|
||||
_REN = "probe-containment-01"
|
||||
|
||||
|
||||
def _toolbox(*args: str) -> subprocess.CompletedProcess[str]:
|
||||
"""Døren som subprosess, `-m`-formen — den virker i en ren klone uten sync."""
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "portfolio_optimiser.toolbox", *args],
|
||||
cwd=_REPO,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _json_file(path: Path, payload: Any) -> str:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
return str(path)
|
||||
|
||||
|
||||
def _stamp(path: Path) -> str:
|
||||
return _json_file(
|
||||
path,
|
||||
{
|
||||
"citations": [
|
||||
{
|
||||
"file": "index.md",
|
||||
"locator": {"start_index": 0, "end_index": 12},
|
||||
"snippet": "energiprofil",
|
||||
}
|
||||
],
|
||||
"model": "ingen-modell",
|
||||
"role": "proposer",
|
||||
"validator_decision": "validated",
|
||||
"token_usage": 0,
|
||||
"cost_baseline_anchored": False,
|
||||
"bundle_id_source": None,
|
||||
"code_forms": {"ENERGI-TOTAL-EL": "identifier"},
|
||||
"external_calls": [],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
#: Dør -> (flagget som navngir utkatalogen, de øvrige argumentene bygget mot en fikstur-mappe).
|
||||
#: Argumentene er de MINIMALE som ellers ville gitt 0 — det er dét som gjør nekten målbar.
|
||||
_DOERER: dict[str, tuple[str, Callable[[Path], list[str]]]] = {
|
||||
"write-run-config": (
|
||||
"--out-dir",
|
||||
lambda f: [
|
||||
"--profile",
|
||||
"local",
|
||||
"--resolved-models",
|
||||
_json_file(f / "modeller.json", {"proposer": "gpt-x"}),
|
||||
"--max-rounds",
|
||||
"5",
|
||||
"--max-tokens",
|
||||
"600000",
|
||||
"--top-k",
|
||||
"3",
|
||||
],
|
||||
),
|
||||
"write-coverage": (
|
||||
"--outbox-dir",
|
||||
lambda f: [
|
||||
"--rows",
|
||||
_json_file(f / "rader.json", []),
|
||||
"--stop-reason",
|
||||
"tokens",
|
||||
],
|
||||
),
|
||||
"write-outbox": (
|
||||
"--outbox-dir",
|
||||
lambda f: [
|
||||
"--proposal",
|
||||
str(_IR),
|
||||
"--provenance",
|
||||
_stamp(f / "stempel.json"),
|
||||
"--verdict-id",
|
||||
"v-probe",
|
||||
],
|
||||
),
|
||||
"write-prepass": (
|
||||
"--outbox-dir",
|
||||
lambda f: [
|
||||
"--declaration",
|
||||
_json_file(f / "kutt.json", {"bundle_id": "x", "considered": 1, "delivered": 1}),
|
||||
],
|
||||
),
|
||||
"write-parse-failures": (
|
||||
"--outbox-dir",
|
||||
lambda f: ["--failures", _json_file(f / "feil.json", [])],
|
||||
),
|
||||
"write-proposal-reviews": (
|
||||
"--outbox-dir",
|
||||
lambda f: ["--payload", _json_file(f / "vurderinger.json", {"reviews": []})],
|
||||
),
|
||||
"write-debate-tools": (
|
||||
"--outbox-dir",
|
||||
lambda f: ["--tool-calls", _json_file(f / "kall.json", [])],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _tree(root: Path) -> dict[str, bytes]:
|
||||
"""Hele filtreet som innhold, ikke bare navn — en dør som overskrev en fil den ikke skulle
|
||||
røre ville hatt samme navneliste."""
|
||||
return {
|
||||
str(p.relative_to(root)): p.read_bytes() for p in sorted(root.rglob("*")) if p.is_file()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("door", sorted(_DOERER))
|
||||
@pytest.mark.parametrize(
|
||||
"form",
|
||||
["relativ-opp", "med-skilletegn", "bare-punktum", "absolutt"],
|
||||
)
|
||||
def test_a_run_id_that_carries_a_path_is_refused_and_writes_nothing(
|
||||
tmp_path: Path, door: str, form: str
|
||||
) -> None:
|
||||
"""Fire former, sju dører. Utkatalogen ligger TO nivåer under `tmp_path`, så en rømning
|
||||
oppover lander et sted armen kan se — den ville vært usynlig om utkatalogen lå på toppen."""
|
||||
fikstur = tmp_path / "fikstur"
|
||||
ut = tmp_path / "inni" / "dypere"
|
||||
flagg, oevrige = _DOERER[door]
|
||||
argv = oevrige(fikstur)
|
||||
run_id = {
|
||||
"relativ-opp": "../../ESCAPE",
|
||||
"med-skilletegn": "under/ESCAPE",
|
||||
"bare-punktum": "..",
|
||||
"absolutt": str(tmp_path / "ABSOLUTT"),
|
||||
}[form]
|
||||
|
||||
foer = _tree(tmp_path)
|
||||
proc = _toolbox(door, flagg, str(ut), "--run-id", run_id, *argv)
|
||||
|
||||
assert proc.returncode != 0, f"{door} godtok en stibærende run-id: {proc.stdout}"
|
||||
assert _tree(tmp_path) == foer, f"{door} la igjen en fil for run-id {run_id!r}"
|
||||
assert not ut.exists(), f"{door} opprettet utkatalogen for en avvist run-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("door", sorted(_DOERER))
|
||||
def test_the_same_call_with_a_clean_run_id_writes(tmp_path: Path, door: str) -> None:
|
||||
"""rc-0-kontrollen. Samme dør, samme argv, eneste forskjell er run-id-en — så nekten over
|
||||
handler om run-id-en og ikke om at kallet var mangelfullt."""
|
||||
fikstur = tmp_path / "fikstur"
|
||||
ut = tmp_path / "inni" / "dypere"
|
||||
flagg, oevrige = _DOERER[door]
|
||||
proc = _toolbox(door, flagg, str(ut), "--run-id", _REN, *oevrige(fikstur))
|
||||
|
||||
assert proc.returncode == 0, proc.stdout + proc.stderr
|
||||
skrevet = json.loads(proc.stdout)
|
||||
stier = [skrevet["path"]] if "path" in skrevet else [skrevet["proposal_path"]]
|
||||
for sti in stier:
|
||||
assert Path(sti).is_file()
|
||||
assert Path(sti).resolve().parent == ut.resolve(), "artefaktet havnet utenfor utkatalogen"
|
||||
|
||||
|
||||
def test_write_coverage_refuses_to_invent_a_stop_reason(tmp_path: Path) -> None:
|
||||
"""«Kjøringen ble ferdig» og «vi fikk aldri vite» må ikke være samme verdi — derfor er
|
||||
`--stop-reason` PÅKREVD og defaulter aldri til tom streng. Regelen er kalt bærende i
|
||||
hovedboken, men ingen arm felte den: en `required=True` byttet til `default=""` passerte hele
|
||||
suiten. Denne armen er vakten — uten flagget skal kallet være en usage-feil, ikke en kjøring
|
||||
som stille kaller en avbrutt runde fullført."""
|
||||
ut = tmp_path / "ut"
|
||||
rader = _json_file(tmp_path / "rader.json", [])
|
||||
|
||||
uten = _toolbox("write-coverage", "--outbox-dir", str(ut), "--run-id", _REN, "--rows", rader)
|
||||
assert uten.returncode == 2, uten.stdout + uten.stderr
|
||||
assert "--stop-reason" in uten.stderr, "nekten må NAVNGI flagget som mangler"
|
||||
assert not ut.exists(), "et kall uten stoppgrunn skal ikke ha lagt igjen noe"
|
||||
|
||||
# rc-0-kontroll: samme argv PLUSS flagget, med den tomme strengen som er en gyldig verdi —
|
||||
# det er nettopp verdien mutanten ville funnet på selv, og den må kalleren kunne oppgi.
|
||||
med = _toolbox(
|
||||
"write-coverage",
|
||||
"--outbox-dir",
|
||||
str(ut),
|
||||
"--run-id",
|
||||
_REN,
|
||||
"--rows",
|
||||
rader,
|
||||
"--stop-reason",
|
||||
"",
|
||||
)
|
||||
assert med.returncode == 0, med.stdout + med.stderr
|
||||
paa_disk = json.loads(Path(json.loads(med.stdout)["path"]).read_text(encoding="utf-8"))
|
||||
assert paa_disk["stop_reason"] == ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue