`--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>
353 lines
14 KiB
Python
353 lines
14 KiB
Python
"""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()
|