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:
parent
986fc19350
commit
2d1264088e
5 changed files with 408 additions and 0 deletions
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
)
|
||||
|
|
|
|||
86
tests/test_full_run_live.py
Normal file
86
tests/test_full_run_live.py
Normal 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, så 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)
|
||||
146
tests/test_live_full_run_contract.py
Normal file
146
tests/test_live_full_run_contract.py
Normal 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, kø-(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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue