portfolio-optimiser/tests/test_toolbox_run_id_containment.py
Kjell Tore Guttormsen 8a6a09df05
test(toolbox): approach-id is the other half of the same composition -- and containment is reachable
`--run-id` was guarded as a class; `--approach-id` was not. `write_outbox` composes
`{run_id}-{approach_id}`, so the same escape ran through the unguarded half: measured 20.09,
`--approach-id a/../../../ESCAPE` answered 0 and put the artefacts three levels above the
directory the caller named.

Counted rather than assumed: 11 path compositions in `outbox.py`, two caller-supplied values in
them. Four forms x the one door approach-id reaches, each with the intermediate directory
created first -- without it two of the four were green at HEAD on a missing directory, which is
a reason that is not the guard.

The containment check is asserted where it IS reachable: the writers themselves, called
directly. That is the run path's own way in -- `run.py` hands its `--run-id` straight to
`outbox.write_*`, past the string rule that lives in the door -- so the arms are not vacuous.

10 arms red on ASSERT about behaviour: 4 approach-id (exit 0), 6 writer-level (DID NOT RAISE).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 12:52:26 +02:00

353 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Ingen kaller-oppgitt NAVN bærer en sti inn i et filnavn — og de sju utboks-dørene håndhever 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.
`--run-id` var bare den ene halvdelen av SAMME sammensetning. `write_outbox` bygger
``{run_id}-{approach_id}`` når en tilnærming er navngitt, og `--approach-id` gikk urørt inn:
``--approach-id a/../../../ESCAPE`` (etter at ``<ut>/<run_id>-a`` finnes) svarte 0 og la
artefaktene TRE nivåer over katalogen kalleren navnga. Telt som klasse: 11 stisammensetninger i
`outbox.py`, to kaller-oppgitte verdier i dem — run-id og approach-id. Begge voktes her.
Og inneslutningen står igjen som SISTE ledd, men der den er OPPNÅELIG: i `outbox.py`, der stien
faktisk settes sammen. Strengregelen bor i døren, så kjørestiens egne kall på `outbox.write_*`
(og `run.py`s eget `--run-id`) går UTENOM den — inneslutnings-armene under kaller skriverne
direkte og er derfor ikke vakuøse. Den verner også mot det NESTE flagget noen legger inn i et
filnavn: sammensetningen er ett sted, og sjekken sitter i den.
"""
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
from portfolio_optimiser import outbox
_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"] == ""
#: De fire formene, som approach-id-en de settes inn som. `write_outbox` bygger stammen
#: ``{run_id}-{approach_id}``, så approach-id-en er et SUFFIKS i sammensetningen og ikke et eget
#: ledd: `../../ESCAPE` alene ville truffet et ikke-eksisterende `<run_id>-..` og gitt en
#: FileNotFoundError — altså grønt av feil grunn. Derfor går den relative formen gjennom et ledd
#: som FINNES (`a/../../../`), som er nøyaktig formen som ble målt 20.09.
_TILNAERMING_FORMER: dict[str, str] = {
"relativ-opp-gjennom-eksisterende": "a/../../../ESCAPE",
"med-skilletegn": "under/ESCAPE",
"bare-punktum": "..",
"absolutt": "/ABSOLUTT",
}
@pytest.mark.parametrize("form", sorted(_TILNAERMING_FORMER))
def test_an_approach_id_that_carries_a_path_is_refused_and_writes_nothing(
tmp_path: Path, form: str
) -> None:
"""`--approach-id` × den ene døren den når. Samme sammensetning, samme krav.
Mellomkatalogen den sammensatte stien peker gjennom opprettes FØRST, og det er ikke pynt:
uten den feiler skrivingen på en manglende katalog, og armen er grønn ved HEAD av en grunn
som ikke er vakten. Målt 20.09 — to av de fire formene var nettopp dét. Med katalogen på
plass svarer ALLE fire 0 og skriver ved HEAD, så hver av dem skiller de to
implementasjonene."""
fikstur = tmp_path / "fikstur"
ut = tmp_path / "inni" / "dypere"
verdi = _TILNAERMING_FORMER[form]
(ut / f"{_REN}-{verdi}-proposal.json").parent.mkdir(parents=True, exist_ok=True)
_, oevrige = _DOERER["write-outbox"]
argv = oevrige(fikstur)
foer = _tree(tmp_path)
proc = _toolbox(
"write-outbox",
"--outbox-dir",
str(ut),
"--run-id",
_REN,
"--approach-id",
verdi,
*argv,
)
assert proc.returncode != 0, f"write-outbox godtok en stibærende approach-id: {proc.stdout}"
assert _tree(tmp_path) == foer, f"write-outbox la igjen en fil for {form}"
def test_the_same_call_with_a_clean_approach_id_writes(tmp_path: Path) -> None:
"""rc-0-kontrollen for approach-id-en. Samme argv, eneste forskjell er verdien — og
artefaktene bærer den utvidede stammen inne i katalogen kalleren navnga."""
fikstur = tmp_path / "fikstur"
ut = tmp_path / "inni" / "dypere"
_, oevrige = _DOERER["write-outbox"]
proc = _toolbox(
"write-outbox",
"--outbox-dir",
str(ut),
"--run-id",
_REN,
"--approach-id",
"a",
*oevrige(fikstur),
)
assert proc.returncode == 0, proc.stdout + proc.stderr
skrevet = json.loads(proc.stdout)
sti = Path(skrevet["proposal_path"])
assert sti.is_file()
assert sti.name == f"{_REN}-a-proposal.json"
assert sti.resolve().parent == ut.resolve(), "artefaktet havnet utenfor utkatalogen"
#: Skriverne bak de sju dørene, hver kalt DIREKTE — utenom strengregelen, som bor i døren.
#: Det er kjørestiens egen vei (`run.py` sender sin `--run-id` rett inn her), så inneslutningen
#: er oppnåelig her og armene er ikke vakuøse.
_SKRIVERE: dict[str, Callable[[str, str], Path]] = {
"write_run_config": lambda d, r: outbox.write_run_config(
d,
r,
profile="local",
resolved_models={"proposer": "gpt-x"},
max_rounds=5,
max_tokens=600000,
top_k=3,
),
"write_coverage": lambda d, r: outbox.write_coverage(d, r, rows=[], stop_reason="tokens"),
"write_prepass": lambda d, r: outbox.write_prepass(d, r, declaration={"bundle_id": "x"}),
"write_parse_failures": lambda d, r: outbox.write_parse_failures(d, r, failures=[]),
"write_proposal_reviews": lambda d, r: outbox.write_proposal_reviews(
d, r, payload={"reviews": []}
),
"write_debate_tools": lambda d, r: outbox.write_debate_tools(d, r, tool_calls=[]),
}
@pytest.mark.parametrize("writer", sorted(_SKRIVERE))
def test_the_writer_itself_refuses_a_path_that_leaves_the_directory(
tmp_path: Path, writer: str
) -> None:
"""Inneslutningen som SISTE ledd, der sammensetningen skjer. En ferdig sti som ikke resolverer
UNDER utkatalogen er en feil, ikke en fil — og ingenting skal være skrevet når den felles."""
ut = tmp_path / "inni" / "dypere"
foer = _tree(tmp_path)
with pytest.raises(ValueError, match="utkatalogen|outside|outenfor|leaves"):
_SKRIVERE[writer](str(ut), "../../ESCAPE")
assert _tree(tmp_path) == foer, f"{writer} la igjen en fil for en rømmende sti"
@pytest.mark.parametrize("writer", sorted(_SKRIVERE))
def test_the_writer_itself_still_writes_a_contained_path(tmp_path: Path, writer: str) -> None:
"""rc-0-kontrollen for inneslutningen: samme kall med et rent navn skriver, inne i katalogen.
Uten den ville en skriver som nektet ALT vært like grønn som den riktige."""
ut = tmp_path / "inni" / "dypere"
sti = _SKRIVERE[writer](str(ut), _REN)
assert sti.is_file()
assert sti.resolve().parent == ut.resolve()