feat(1b): skjemaet ER akseptert av det levende endepunktet - malt, ikke resonnert [skip-docs]

Fase 1b siste trinn: forste bundne levende kjoring over HELE run_project-stien
mot Foundry (gpt-4-1-mini). Okt 37s uttalte aerlighets-grense er lukket ved
maling: ingen -parse-failures.json i outboksen, altsa parset hvert eneste
genererings-svar. assumptions-normaliseringen virket ende-til-ende.

Utfall: rejected pa P90 (claimed 34500 > feasible 11488), checker approve.
Kjoringen KONKLUDERTE - per pre-registreringen det bestatte utfallet. De to
falsifisererne skilte lag for forste gang mot en levende modell.

FUNN storre enn den gronne testen: modellen fant opp kostkoden
EL-LIGHTING-OP-HR (null treff i kunnskapsbasen). Avvisningen var riktig men
skjedde pa 30%-cap-en, ikke stage 0 - bundelen shipper ingen cost-baseline.json,
sa S4.0-forankringen var inaktiv. Ko-fort, ikke rettet her.

Gate-designet er ovis beslutning, tatt for koding:
- TREDJE distinkt opt-in PORTFOLIO_LIVE_FULL_RUN (truthiness, 4b-invarianten).
  Begge eksisterende live-tester gater pa SAMME to Foundry-variabler, sa
  gjenbruk ville latt den billige proben fyre den dyre kjoringen - stigen i
  maleprotokollen ville kollapset til ett trinn. MALT: den dyre SKIPPET med
  begge Foundry-variablene satt.
- Asserten i EN kopi (conftest.assert_full_run_contract, ko-(p)), smal med
  vilje: fravaer av parse-failures-artefaktet + at validatoren avgjorde. En
  rejected BESTAR - pastanden er schema-aksept, ikke modell-dommekraft.
- Iron Law uten a betale to ganger: diskrimineringen bevist OFFLINE av
  test_live_full_run_contract.py. To mutasjoner, hver sin signatur: detach
  artefakt-sjekken (T1 rod ALENE) - raise ubetinget (T2 rod ALENE).

STATE-premiss korrigert: "test_foundry_profile_live dekker KUN klient-nivaet"
var upresist - test_portfolio_live.py dekket allerede fan-outen, men dens
len(runs)==1 kan ikke skille validert fra avvist og bar derfor ikke pastanden.

869 passed / 5 skipped (fra 867/4), ruff+format+mypy rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF7va4cpRiuf79kTzAi3vW
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 20:02:26 +02:00
commit 2d1264088e
5 changed files with 408 additions and 0 deletions

View file

@ -429,6 +429,29 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
`run.py` · endre avrundingsmodus · la `realize` gå utenom `to_ore`. **Ærlighets-grense:**
`sum_claimed_saving_nok` (`run.py:_aggregate`) er BEVISST urørt — et float-NOK-rapportfelt som
aldri kvantiseres og aldri sammenlignes mot ledgeren, altså utenfor ordens-defekten.
- **En BETALT test får sin EGEN opt-in, og instrumentet bevises GRATIS (Fase 1b, siste trinn):**
`tests/test_full_run_live.py` kjører hele `run_project`-stien mot et ekte Foundry-deployment, og
gates på **fire** ting — de to Foundry-variablene, `PORTFOLIO_MODEL_MAP`, og et TREDJE, distinkt
`PORTFOLIO_LIVE_FULL_RUN` lest på **truthiness** (4b-invarianten). **Den tredje variabelen er
load-bearing, ikke pynt:** `test_foundry_profile_live.py` (klient-probe) og `test_portfolio_live.py`
(fan-out) gater på nøyaktig SAMME to variabler, så å gjenbruke det paret ville betydd at en
operatør som eksporterer dem for den BILLIGE ett-ords-proben også fyrer den dyre fullkjøringen —
altså at måleprotokollens stige («bevis så mye som mulig før det dyre trinnet, så en feil er
attribuerbar») kollapser til ett trinn. **MÅLT:** med begge Foundry-variablene satt SKIPPET den
dyre, og den billige var grønn. `PORTFOLIO_MODEL_MAP` er med av en annen grunn — attribusjon: uten
den feiler kjøringen av en KONFIGURASJONS-årsak som ser ut som en modell-feil.
**Asserten bor i ÉN kopi** (`conftest.assert_full_run_contract`, kø-(p)) og er smal med vilje:
fraværet av `{run_id}-parse-failures.json` (økt 35-invarianten «filens tilstedeværelse er
signalet») + at `validator_decision` avgjorde. **En `rejected` BESTÅR** — påstanden som felles er
at det strukturerte skjemaet ER akseptert av det levende endepunktet, ikke at modellen resonnerer
godt; å kreve `validated` ville vært en modell-dømmekraft-påstand ingen enkelt kjøring kan bære.
**Iron Law uten å betale to ganger:** et betalt kall kan ikke kjøres rødt-så-grønt, så
diskrimineringen bevises OFFLINE av `tests/test_live_full_run_contract.py` — to armer over samme
helper (én parse-feil → kontrakten MÅ feile; alle parser → MÅ passere). Load-bearing MÅLT, to
mutasjoner med hver sin distinkte signatur: detach artefakt-sjekken (T1 rød ALENE — kontrakten
degraderer da til `test_portfolio_live.py`s `len(runs)==1`-klasse) · raise ubetinget (T2 rød
ALENE — den motsatte vakuiteten, en live-test som bare kan bli rød). **Det betalte kallet er
MÅLINGEN, aldri beviset på at måleinstrumentet virker.**
- **Kostnadsdisiplin:** utvikle primært på lokal profil (gratis); Foundry/Azure (privat tenant finnes) kun til målrettet, minimal verifisering; billigste modeller + små syntetiske data + harde token-tak. Ingen tunge test-kjøringer.
- **Offline simulering = primært metode-bevis (kostnadsdrevet, erstatter §11.8):** operatøren kjører
IKKE MAF mot ekte modell (verken Azure/Foundry eller Ollama — API for begge repoene er for kostbart

View file

@ -108,3 +108,107 @@ serverfeil. Det er ikke rettet her; det er notert.
den hostede flaten.
4. Først når 1 og 2 er på plass: en ny bundet kjøring, og en gatet test som dekker **hele
`run_project`-stien** (den eksisterende gatede testen dekker kun klient-nivået).
**Rettelse til punkt 4, målt 14.08 (økt 40):** parentesen er upresis. `tests/test_portfolio_live.py`
fantes allerede og dekker `run_portfolio`-utfoldingen — altså hele `run_project`-stien — med samme
env-gate. Men den asserterer `len(result.runs) == 1`, og fordi `runs` og `failures` *partisjonerer*,
kan den kun skille «kjøringen fullførte» fra «kjøringen raiste». Den kan **ikke** skille et validert
forslag fra et avvist, og heller ikke en kjøring der svarene parset fra en der de ikke gjorde det.
Den bærer derfor ikke påstanden 1b skal felle. Punkt 4 står, men grunnen er en annen enn skrevet.
## 5. Pre-registrerte utfall (skrevet FØR kjøringen)
Denne kjøringen har ett formål: å felle den ene gjenstående ærlighets-grensen fra økt 37 — **at det
emitterte `response_format`-skjemaet ER akseptert av det LEVENDE endepunktet er uverifisert**;
testene beviser konformitet med Azures *dokumenterte* subset, ikke aksept.
**Instrumentet** er `conftest.assert_full_run_contract`, og diskriminatoren er et artefakt repoet
allerede eier: `{run_id}-parse-failures.json` skrives hvis og bare hvis et svar ikke lot seg parse
(økt 35). Artefaktets **fravær** ved siden av et `RunResult` beviser at hvert genererings-svar kom
tilbake i den bestilte formen. Kontraktens evne til å skille er bevist **offline og gratis**
(`tests/test_live_full_run_contract.py`, to mutasjoner med hver sin signatur: detach artefakt-sjekken
→ T1 rød alene; raise ubetinget → T2 rød alene). Det betalte kallet er *målingen*, ikke beviset på at
måleinstrumentet virker.
**Hva hvert utfall betyr — avgjort på forhånd:**
| Utfall | Betydning |
|---|---|
| Ingen parse-failure-artefakt + validatoren avgjorde (`validated` **eller** `rejected`) | **Ærlighets-grensen er felt.** Skjemaet ble akseptert av det levende endepunktet. En P90-avvisning er et *bestått* utfall — kjøringen KONKLUDERTE. |
| Parse-failure-artefaktet finnes | Skjemaet ble **ikke** honorert. Et FUNN, ikke et bevis — og denne gangen finnes den råe teksten (økt 35), så neste steg kan begrunnes i stedet for gjettes. |
| `BudgetExceeded` | Fortsatt ubevist, men artefaktet forklarer hvorfor. Samme form som 1a-kjøringen. |
**Taket heves IKKE.** `max_rounds`/`max_tokens` står på defaultene den første levende kjøringen døde
på: fyrer ledgeren igjen, er dét informasjon, og å heve taket ville brukt mer penger på en sti som
kanskje fortsatt er brukket.
**Gate-variabelen er en TREDJE, distinkt opt-in** (`PORTFOLIO_LIVE_FULL_RUN`, lest på *truthiness*).
Begge de eksisterende live-testene gater på nøyaktig `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` +
`PORTFOLIO_FOUNDRY_DEPLOYMENT`; å gjenbruke det paret ville betydd at en operatør som eksporterer de
to for å kjøre den **billige** ett-ords-proben også fyrer den dyre fullkjøringen — altså at stigen i
§1 kollapser til ett trinn. `PORTFOLIO_MODEL_MAP` er med i skip-betingelsen av en annen grunn:
uten den feiler kjøringen av en *konfigurasjons*-årsak som ser ut som en modell-feil.
## 6. Den bundne fullkjøringen — målt 14.08 kl. 19:54
Stigen på nytt, samme disiplin som §1. Alt under er kjørt.
| Trinn | Kommando | Utfall |
|---|---|---|
| 1a | `az account get-access-token --resource https://ai.azure.com` (uten rør) | exit 0, `expiresOn 21:07:40` |
| 1b | `az ad signed-in-user show` (ekte Graph-kall, ikke lokal cache) | exit 0 — gmail-kontoen, ikke jobbkontoen |
| 2 | `preflight --profile azure` | `preflight OK (azure)` |
| 3 | `pytest tests/test_foundry_profile_live.py` | **1 passed, 5,48 s** |
| 3b | samme kall, med `test_full_run_live.py` samlet | **SKIPPED** — det tredje flagget holder stigen |
| 4 | `pytest tests/test_full_run_live.py` (`PORTFOLIO_LIVE_FULL_RUN=1`) | **1 passed, 22,87 s** |
Trinn 3b er verdt å uttale: den dyre testen hoppet over **selv med begge Foundry-variablene satt**.
Det er den empiriske bekreftelsen på at det tredje opt-in-flagget gjør jobben sitt design lover.
### Ærlighets-grensen ER felt
Outboksen inneholder `-proposal.json`, `-outcome.json`, `-runconfig.json` — og **ingen
`-parse-failures.json`**. Hvert eneste genererings-svar fra `gpt-4.1-mini` kom tilbake som et
parsebart objekt i den bestilte formen. Økt 37s uttalte grense — *«at det emitterte skjemaet ER
akseptert av det LEVENDE endepunktet er IKKE verifisert»* — er dermed **lukket ved måling**, ikke
ved resonnement. Kontrasten til den første levende kjøringen er hele funnet: der brant tolv runder
på formatfeil og etterlot null tegn; her feilet ingen.
`assumptions` kom tilbake som forventet (`{"EL-LIGHTING-OP-HR": [10.0, 13.0]}`), altså virker økt 37s
additive wire-form → IR-map-normalisering ende-til-ende mot et levende endepunkt. Det var
beslutningen som holdt den stokastiske falsifisereren fra å gå inert, og den er nå prøvd i felt.
### Utfallet: `rejected` — og det er et bestått utfall
```
outcome_type: rejected
reason: claimed saving 34500 exceeds P90 feasible 11488
checker_verdict: approve
validator_decision: rejected
token_usage: 15 306
```
Kjøringen KONKLUDERTE. Per §5s pre-registrering er dette det positive utfallet: validatoren ble nådd
med en parsebar kandidat og avgjorde. **Og de to falsifisererne skilte lag for første gang mot en
levende modell** — checkeren godkjente *resonnementet*, validatoren avviste *tallene*. Nøyaktig den
uavhengigheten `checker_verdict` holdes atskilt fra `provenance.validator_decision` for.
### FUNN som er viktigere enn den grønne testen: modellen fant opp en kostkode
Forslaget bar `code: "EL-LIGHTING-OP-HR"` (3 000 × 11,5). **Den koden finnes ikke noe sted i
kunnskapsbasen** (`grep` over hele bundelen: null treff). Kunnskapsbasen instruerer eksplisitt
mappingen `ENERGI-TOTAL-EL`, 300 000 kWh × 1,00 NOK — modellen konstruerte i stedet sin egen
kostlinje med en egen enhet.
Avvisningen var derfor **riktig, men skjedde på feil gate**: 30 %-cap-en fanget den på *magnitude*
(34 500 > P90 11 488), ikke stage 0 på *eksistens*. Grunnen er en kjent og uttalt egenskap, ikke en
defekt: S4.0-forankringen aktiveres på bundle-stien KUN når bundelen shipper `cost-baseline.json`,
og `bygg-energi-mikro` gjør ikke det (målt) — «en pre-amendment-bundle er legitimt uforankret».
Dette er akkurat den hallusinasjons-klassen S4.0 ble bygget for, observert i felt for første gang.
At den uforankrede gaten fanget den likevel er betryggende; at den fanget den på den dyre gaten
i stedet for den billige er en kø-post, ikke noe som endres her.
**Fortsatt ikke bevist:** at systemet produserer et *validert* forslag mot en levende modell. Denne
kjøringen avviste — korrekt, og med en begrunnelse som kan leses. Én kjøring er én kjøring, og
`gpt-4.1-mini`s egnethet for proposer-rollen er ikke avgjort av den.

View file

@ -192,3 +192,52 @@ def docs_dir(tmp_path) -> str:
encoding="utf-8",
)
return str(d)
# ------------------------------------------------------------------------------------------------
# Fase 1b — the FULL-RUN contract, in ONE copy (kø-(p): a second copy of an assertion drifts).
# ------------------------------------------------------------------------------------------------
#: What the paid run must fell, stated as an assertion rather than as prose. The open honesty limit
#: after økt 37 is narrow and specific: *"that the emitted structured schema IS accepted by the LIVE
#: endpoint is NOT verified — the tests prove conformance with the DOCUMENTED subset, not
#: acceptance."* So the contract asserts schema ACCEPTANCE, never model JUDGEMENT.
#:
#: The discriminator is an artefact this repo already owns: ``{run_id}-parse-failures.json`` is
#: written if and ONLY if some reply failed to parse (økt 35 invariant — "the file's presence is the
#: signal"). Its ABSENCE beside a RunResult therefore proves that every generation reply came back
#: in the requested shape, which is exactly what "the live endpoint honoured the schema" means.
#:
#: ``validator_decision`` is the second half: it mirrors the VALIDATOR alone (never the checker), so
#: reading it proves the deterministic gate actually ran on a parsed candidate. Both ``validated``
#: and ``rejected`` satisfy the contract — a P90 rejection is a run that CONCLUDED, and demanding
#: ``validated`` would be asserting that the model reasons well, which no schema can promise and
#: which one paid run could not establish anyway.
def assert_full_run_contract(result, outbox_dir, run_id: str) -> None:
"""Assert the Fase 1b full-run contract on a completed ``run_project`` result.
Two things, and deliberately nothing else:
1. **No parse-failure artefact** every reply parsed, i.e. the live endpoint accepted the
emitted ``response_format`` schema. This is the honesty limit being felled.
2. **The validator was reached and decided** ``provenance.validator_decision`` is one of the
two decisions the deterministic gate emits.
Deliberately NOT asserted: ``checker_verdict``, token counts, ``validated_count``, or the
content of the proposal. Those are model-judgement claims, and one run cannot carry them.
"""
from pathlib import Path
artefact = Path(outbox_dir) / f"{run_id}-parse-failures.json"
if artefact.exists():
# Quote the evidence in the failure message: the whole point of økt 35 was that the operator
# should never again have to guess WHY a reply did not parse.
raise AssertionError(
f"the endpoint did NOT honour the structured schema — {artefact.name} exists.\n"
f"{artefact.read_text(encoding='utf-8')[:2000]}"
)
assert result.provenance.validator_decision in {"validated", "rejected"}, (
"the deterministic validator never decided — the run did not reach the gate with a "
f"parsed candidate (validator_decision={result.provenance.validator_decision!r})"
)

View file

@ -0,0 +1,86 @@
"""Fase 1b, last step — GATED live run over the WHOLE ``run_project`` path (måleprotokoll §4.4).
NOT default CI, and NOT gated like its two siblings. This is the expensive arm: it drives the
complete vertical slice bundle navigation, the maker/checker debate, generation under
``response_format``, and the deterministic validator against a real Foundry deployment.
**Why a THIRD environment variable, and why it is load-bearing.** ``test_foundry_profile_live.py``
(client-level probe) and ``test_portfolio_live.py`` (``run_portfolio`` fan-out) both skip on exactly
``PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`` + ``PORTFOLIO_FOUNDRY_DEPLOYMENT``. Reusing that pair here
would mean the moment an operator exports the two variables to run the CHEAP one-word probe, this
full run fires too collapsing the måleprotokoll's whole point (§1: *"bevis så mye som mulig før
det dyre trinnet, en feil er attribuerbar"*) into a single step, and spending money on a rung
whose predecessors have not been shown green. ``PORTFOLIO_LIVE_FULL_RUN`` is therefore a separate,
deliberate opt-in, read on **truthiness, not presence** (the Fase 4b invariant: an exported-but-empty
value is a shell accident, not a decision).
``PORTFOLIO_MODEL_MAP`` is part of the skip condition for a different reason attribution. ``run.py``
stamps provenance with the deployment NAME before any client is built (målt 4e), so without the map
the run fails for a CONFIGURATION reason while looking exactly like a model failure. Skipping is
honest; failing there would misattribute.
**What this asserts is narrow on purpose** see ``conftest.assert_full_run_contract``. The claim
being felled is *"the emitted structured schema is accepted by the live endpoint"* (økt 37's stated
honesty limit), NOT "the model proposes well". A validator REJECTION passes this test: the run
reached the deterministic gate with a parsed candidate, which is the whole question. The contract's
ability to discriminate is proven offline and for free by
``tests/test_live_full_run_contract.py`` the paid call here is the measurement, not the proof that
the instrument works.
Outcomes are pre-registered in ``docs/2026-08-14-fase1b-forste-levende-kjoring.md`` §5, written
BEFORE the run, so the write-up cannot be negotiated after the fact.
The round/token caps are the SAME ones the first live run died on. They are deliberately not
raised: if the ledger fires again that is information, and raising it spends more on a path that may
still be broken.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from conftest import assert_full_run_contract
from portfolio_optimiser.run import RunResult, run_project
from portfolio_optimiser.verdicts import VerdictStore
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
_PROJECT_ID = "BYGG-KONTOR-NORD"
_ENDPOINT = os.environ.get("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT")
_DEPLOYMENT = os.environ.get("PORTFOLIO_FOUNDRY_DEPLOYMENT")
_MODEL_MAP = os.environ.get("PORTFOLIO_MODEL_MAP")
#: Truthiness, not presence (Fase 4b): ``PORTFOLIO_LIVE_FULL_RUN=`` must NOT arm a paid run.
_OPTED_IN = bool(os.environ.get("PORTFOLIO_LIVE_FULL_RUN"))
_SKIP = not (_ENDPOINT and _DEPLOYMENT and _MODEL_MAP and _OPTED_IN)
@pytest.mark.skipif(
_SKIP,
reason=(
"paid full run not armed (set PORTFOLIO_LIVE_FULL_RUN=1 alongside "
"PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT + PORTFOLIO_FOUNDRY_DEPLOYMENT + PORTFOLIO_MODEL_MAP)"
),
)
async def test_full_run_reaches_the_validator_on_a_live_model(tmp_path: Path) -> None:
"""The whole slice against a real deployment: every generation reply must come back in the
requested shape, and the deterministic validator must decide on it."""
outbox_dir = tmp_path / "outbox"
run_id = "live-full-001"
result = await run_project(
_PROJECT_ID,
"azure",
docs_dir=str(BUNDLE_DIR),
bundle_dir=str(BUNDLE_DIR),
verdict_input={"decision": "approved", "rationale": "expert reviewed (live 1b)"},
store=VerdictStore(verdicts=[]),
outbox_dir=str(outbox_dir),
run_id=run_id,
)
assert isinstance(result, RunResult)
assert_full_run_contract(result, outbox_dir, run_id)

View file

@ -0,0 +1,146 @@
"""The Fase 1b full-run contract must DISCRIMINATE — proven offline, for free (Fase 1b, last step).
``tests/test_full_run_live.py`` spends real money on a real endpoint, exactly once, and cannot be
run red-then-green: the paid call is the MEASUREMENT, not the proof that the measuring instrument
works. This file is that proof, and it costs nothing.
The repo's own rule — *"en test som ikke kan skille to implementasjoner beviser ingenting"* — has
been violated by this project's own measuring instruments three times (økt 27's slide sweep read
``section``'s 100vh and reported the same number for all eleven slides; økt 37's T3 asserted on a
schema the scripted client ignores). An assertion that can only ever pass is the same defect class,
and a LIVE assertion is the worst place to discover it, because a green result there is precisely
what the operator would act on.
So both arms drive the SAME ``assert_full_run_contract`` helper the live test uses (ONE copy, in
``conftest`` a second copy would drift, -(p)), over the SAME bundle and the SAME project as the
live run, through the canonical ``ScriptedChatClient``'s ``reply_selector`` seam:
* T1 a run in which ONE reply failed to parse must make the contract FAIL. This is the arm that
matters: it is the offline stand-in for "the live endpoint ignored ``response_format``", which is
the single outcome the paid run exists to rule out. Per the repo rule for negative asserts, it
first PROVES the event happened (the artefact exists) rather than inferring it from the failure.
* T2 the CONTROL: a run in which every reply parsed must make the contract PASS. Without it, a
helper that raised unconditionally would satisfy T1 and the live test could then only ever be red,
which is the mirror-image vacuity.
Together they show the contract keys on the ACTUAL discriminator (the parse-failure artefact), not
on something both runs share.
"""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from conftest import assert_full_run_contract
from portfolio_optimiser.budget import Budget, TokenMeter
from portfolio_optimiser.run import RunResult, run_project
from portfolio_optimiser.simulation import ScriptedChatClient
from portfolio_optimiser.verdicts import VerdictStore
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
_PROJECT_ID = "BYGG-KONTOR-NORD"
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (contract probe)"}
#: The line ``generate._build_messages`` puts in EVERY generation prompt and nowhere else — the one
#: identifier separating a generation call from a debate turn (borrowed from
#: ``test_parse_failure_capture_loadbearing``, whose seam this file shares).
_GENERATION_MARK = "Respond with ONLY a JSON object"
#: Prose where an object was requested: what a model that ignored ``response_format`` returns.
_MALFORMED = (
"Sure! Here is what I found for this project.\n\n"
"The main opportunity looks like demand-controlled lighting, worth roughly 30 000 NOK.\n"
"Let me know if you want that as JSON."
)
#: BYGG-KONTOR-NORD: affected total 300000 x 1.0 -> degenerate Monte Carlo P90 = 90000, so a claim
#: of 30000 validates (same arithmetic as ``test_parse_failure_capture_loadbearing``).
_VALID_REPLY = (
'{"measure":"Behovsstyrt belysning i fellesarealer","affected_items":'
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],'
'"claimed_saving_nok":30000}'
)
def _malformed_then_valid(failures: int) -> Callable[[str, str], str]:
"""Fail to parse ``failures`` times, then answer with a proposal that validates. Keyed on the
GENERATION prompt only, so debate turns which are never parsed do not consume the counter."""
seen = {"n": 0}
def _select(blob: str, _role: str) -> str:
if _GENERATION_MARK not in blob:
return "ok"
seen["n"] += 1
return _MALFORMED if seen["n"] <= failures else _VALID_REPLY
return _select
async def _run(select: Callable[[str, str], str], outbox_dir: Path, run_id: str) -> RunResult:
def factory(role: str) -> ScriptedChatClient:
return ScriptedChatClient(role=role, reply_selector=select, default_reply="ok")
result = await run_project(
_PROJECT_ID,
"local",
docs_dir=str(BUNDLE_DIR),
bundle_dir=str(BUNDLE_DIR),
verdict_input=_VERDICT_INPUT,
store=VerdictStore(verdicts=[]),
client_factory=factory,
outbox_dir=str(outbox_dir),
run_id=run_id,
meter=TokenMeter(Budget(max_tokens=10**9, max_rounds=8)),
)
assert isinstance(result, RunResult)
return result
# --------------------------------------------------------------------------------------------
# T1 — the arm that matters: an unparsed reply must FAIL the contract.
# --------------------------------------------------------------------------------------------
async def test_contract_fails_when_a_reply_did_not_parse(tmp_path: Path) -> None:
"""The offline stand-in for "the live endpoint ignored the schema". The run itself SUCCEEDS —
the second reply validates so the contract cannot be keying on the run's outcome; the only
thing separating this from T2 is the parse-failure artefact.
RED (i.e. this test fails) on a contract that ignores the artefact and merely checks that a
RunResult came back which is what ``test_portfolio_live.py``'s ``len(runs) == 1`` does, and
the reason that existing gated test could not carry this claim."""
outbox_dir = tmp_path / "outbox"
run_id = "contract-dirty"
result = await _run(_malformed_then_valid(1), outbox_dir, run_id)
# Prove the event happened FIRST — a negative assert that merely observes a failure cannot tell
# "the contract caught the artefact" from "the contract failed for some unrelated reason".
artefact = outbox_dir / f"{run_id}-parse-failures.json"
assert artefact.exists(), "precondition: this arm must actually produce a parse failure"
assert result.provenance.validator_decision in {"validated", "rejected"}, (
"precondition: the run must otherwise CONCLUDE, so the artefact is the only difference"
)
with pytest.raises(AssertionError, match="did NOT honour the structured schema"):
assert_full_run_contract(result, outbox_dir, run_id)
# --------------------------------------------------------------------------------------------
# T2 — the CONTROL: a clean run must PASS.
# --------------------------------------------------------------------------------------------
async def test_contract_passes_when_every_reply_parsed(tmp_path: Path) -> None:
"""Without this control, a helper that raised unconditionally would satisfy T1, and the live
test could then only ever be red the mirror image of a check that can only ever be green."""
outbox_dir = tmp_path / "outbox"
run_id = "contract-clean"
result = await _run(_malformed_then_valid(0), outbox_dir, run_id)
assert not (outbox_dir / f"{run_id}-parse-failures.json").exists()
assert_full_run_contract(result, outbox_dir, run_id)