feat(simulation): P4 pkt. 3+4 — demo-transkriptet pinnet, frø-setningen avledet [skip-docs]
Kriterium 6 er selv-identitet: to kjøringer av en regredert demo er like enige som to av en riktig. Fasiten forlater derfor prosessen. stdout pinnes ORDRETT (og er dermed demoens abortsti); stderr normaliseres på nøyaktig to MÅLTE miljø-spann — site-packages-prefikset og temp-katalogen — med po-sim- holdt synlig, fordi det er en egenskap ved programmet og ikke ved miljøet. Pinnet stderr = fire linjer. Kontrollen som forbyr at masken vokser er load-bearing: en droppende normaliserer med fasiten regenerert under seg holder BEGGE likhets-testene grønne. Pkt. 4: planens forhåndsskrevne frø-setning sa «én av de TO tidligere dommene». Målt mot levert VEGLYS-bundle henter Kjøring B TRE — én fulgte med kunnskapsbasen, to er demoens egne, én per tidsskala. Splitten avledes derfor fra kjøringen; en håndskrevet «én av tre» ville vært den andre kopien som drifter. Fem mutasjoner alle røde + grønn kontroll (hele suiten hver gang): ett byte i en stdout-linje · detach dempingen · over-normaliser stderr · literal splitt · detach frø-setningens print. Byte- og detach-mutasjonene ble fanget av KUN golden-testen; den literale splitten av KUN skille-testen. 793 -> 801 passed / 4 skipped.
This commit is contained in:
parent
7aef1feaa4
commit
50232fb88d
7 changed files with 422 additions and 2 deletions
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -345,6 +345,31 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
|
|||
(`tests/test_demo_stderr_quiet_loadbearing.py`), fem mutasjoner røde + grønn kontroll: detach
|
||||
`main()`-kallet (subprosess-testen er ENESTE som fanger det — de tre filter-testene installerer
|
||||
filteret selv) · la filteret droppe alt · installer ved import · pluss de to entry-point-mutasjonene.
|
||||
- **Demo-transkriptet er sjekket inn som fasit, og masken er SPANN-avgrenset (P4 pkt. 3):**
|
||||
kriterium 6 er selv-identitet — to kjøringer av en REGREDERT demo er like enige som to kjøringer av
|
||||
en riktig, så fasiten må forlate prosessen. `tests/golden/demo-transcript.stdout` er stdout ORDRETT
|
||||
(målt byte-identisk over kjøringer OG i fersk klon), og er derfor også demoens abortsti: feiler
|
||||
live-kjøringen, ER fila transkriptet. `….stderr` er normalisert på nøyaktig to MÅLTE miljø-spann —
|
||||
`site-packages`-prefikset og temp-katalogen bak `(arbeidskopi: …)`, der `po-sim-`-prefikset holdes
|
||||
SYNLIG fordi det er en egenskap ved programmet (`mkdtemp(prefix=…)`), ikke ved miljøet; pinnet
|
||||
stderr er fire linjer. **Masken må ikke kunne vokse:** P4 pkt. 2 betalte for at en NY advarsel
|
||||
fortsatt når stderr, og en normalisering som maskerte hele linjer ville opphevet det i ett trekk —
|
||||
derfor er `test_normalisation_does_not_mask_a_new_warning` kontrollen som forbyr det (MÅLT: en
|
||||
droppende normaliserer med fasiten regenerert under seg holder BEGGE likhets-testene grønne og
|
||||
felles kun av kontrollen). `PYTHONIOENCODING` pinnes, ellers måler sammenligningen operatørens
|
||||
locale i stedet for programmet. **Regenerering er en beslutning, aldri rydding** — fasiten kan ikke
|
||||
bevise sin egen kjøring, den pinner outputen P3-kriteriene ble målt mot.
|
||||
- **Frø-setningen AVLEDES fra kjøringen (P4 pkt. 4):** demoen sier høyt hvor Kjøring B's tidligere
|
||||
dommer kommer fra, og splitten (`_verdict_origin_line`) regnes ut — linja rett over printer allerede
|
||||
antallet, så en håndskrevet «én av tre» ville vært den andre kopien som drifter (samme regel som
|
||||
pkt. 0-baselinen), og ville blitt sagt uendret etter at en framtidig bundle shipper en ANDRE frøsatt
|
||||
dom. Klassifisereren er de to markørene demoen alt sporer; den hviler på at den frøsatte dommen
|
||||
bærer INGEN av dem, som MÅLES på levert bundle. **Planens forhåndsskrevne ordlyd var FEIL mot
|
||||
levert innhold** («én av de TO») — målt henter Kjøring B TRE: én fulgte med kunnskapsbasen, to er
|
||||
demoens egne (én per tidsskala). Load-bearing MÅLT
|
||||
(`tests/test_p4_honesty_sentences_loadbearing.py` + golden-transkriptet), fem mutasjoner alle røde
|
||||
+ grønn kontroll: ett byte i en stdout-linje · detach dempingen · over-normaliser stderr · literal
|
||||
splitt (fanget av INGENTING i 800 tester bortsett fra skille-testen) · detach frø-setningens print.
|
||||
- **Delt ekspert-persona som Agent Skill (§8, framework-nøytral):** ekspert-reviewer-personaen bor i
|
||||
`shared/skills/expert-reviewer/` (`SKILL.md` + `references/example-verdict.json`) og er den ENE
|
||||
delte artefakten begge stacker instansierer reviewer-en fra. `shared/` forblir REN DATA — MAF-siden
|
||||
|
|
|
|||
|
|
@ -257,10 +257,11 @@ et planlagt utfall, ikke en krise — generalprøve nr. 0 mandag har allerede ve
|
|||
IKKE være literal — de to gjenstående `ExperimentalWarning`-linjene bærer en absolutt sti inn i
|
||||
`.venv/…/site-packages`, som er ulik i fersk klon og arbeidskopi (målt). Normaliser på BEGGE:
|
||||
site-packages-stien OG `po-sim-`-suffikset. Pinnet stderr er da fire linjer, ikke to.
|
||||
3. **Golden-transkript:** sjekk inn demo-outputen som fasit-fil og diff mot den — selvidentitet
|
||||
3. **Golden-transkript ✔ (2026-08-09)** — se UTFØRT-blokka under punkt 4.
|
||||
*(Planteksten:)* sjekk inn demo-outputen som fasit-fil og diff mot den — selvidentitet
|
||||
(kriterium 6) fanger ikke-determinisme, men ikke regresjon mellom onsdag og torsdag.
|
||||
**Transkriptet er også demoens abortsti** (gjort eksplisitt i P4.5).
|
||||
4. **To ferdigskrevne setninger** inn i ærlighets-teksten (mønsteret fra innholdsgate-planen §5).
|
||||
4. **To ferdigskrevne setninger ✔ (2026-08-09)** inn i ærlighets-teksten (mønsteret fra innholdsgate-planen §5).
|
||||
NO-GO-varianten er OMSKREVET etter I1: «kostnads-forankringen er aktiv også i reserve-eksemplet,
|
||||
men kostnadstallene der er syntetiske — avledet av manuset, ikke levert av et fagmiljø»
|
||||
(fallback hvis pkt. 0 faller: den gamle setningen «kostnads-forankringen er ikke aktiv i
|
||||
|
|
@ -269,6 +270,42 @@ et planlagt utfall, ikke en krise — generalprøve nr. 0 mandag har allerede ve
|
|||
5. **`[project.scripts]` (flyttet HIT fra S1.c, I3) ✔ (2026-08-09, `ab7f45a`)** — se UTFØRT-blokka
|
||||
rett under.
|
||||
|
||||
**UTFØRT — punkt 3 og 4 (2026-08-09). 793 → 801 passed / 4 skipped.** Punktene ble gjort i ÉN økt
|
||||
og i denne rekkefølgen fordi pkt. 4 endrer stdout: en fasit pinnet før den ville vært foreldet i
|
||||
samme økt. Testene ble skrevet FØR begge (målt rød: pkt. 3 på manglende fasit-fil, pkt. 4 på
|
||||
manglende `_verdict_origin_line`), og fasiten ble generert til slutt — ETTER at P3-kriteriene var
|
||||
re-verifisert mot den nye outputen (åtte steg-linjer; `REJECTED` på P90-stagen og `VALIDATED 445500`
|
||||
for samme kandidat; begge markører `False` i Kjøring A og `True` i B; kostbaselinen erklært).
|
||||
|
||||
*Punkt 3:* `tests/golden/demo-transcript.stdout` er ORDRETT (ingen normalisering, ingen toleranse) —
|
||||
det gjør fila brukbar som abortsti, siden den ER det operatøren ville sett. `….stderr` normaliserer
|
||||
nøyaktig de to spannene som ble målt miljø-avhengige: `site-packages`-prefikset og temp-katalogen bak
|
||||
`(arbeidskopi: …)`. `po-sim-`-prefikset holdes SYNLIG — det tilhører programmet, ikke miljøet — mens
|
||||
`TMPDIR`-rota og suffikset maskeres. Pinnet stderr = fire linjer, som pkt. 2 forutsa. Bredden på
|
||||
maskeringen er selv under test: `test_normalisation_does_not_mask_a_new_warning` mater en syntetisk
|
||||
EKSTRA linje gjennom samme normaliserer og krever at den slutter å matche.
|
||||
|
||||
*Punkt 4 — den forhåndsskrevne frø-setningen var FEIL, og målingen fanget det.* Planen sa «én av de
|
||||
to tidligere dommene i Kjøring B fulgte med eksempelet». Målt mot levert VEGLYS-bundle henter
|
||||
Kjøring B **tre**: den frøsatte (`verdict-veglys-fro.md`), Steg-8-promoteringen og Steg-7-innboksen —
|
||||
altså **én fulgte med, to er demoens egne, én per tidsskala**. Setningen ville vært en falsk påstand
|
||||
sagt på scenen om et tall som står printet linja over. Splitten er derfor **avledet**
|
||||
(`_verdict_origin_line`), ikke skrevet ned: en håndskrevet «én av tre» er den andre kopien som
|
||||
drifter (samme regel som punkt 0), og ville blitt sagt uendret etter at en framtidig bundle shipper
|
||||
en andre frøsatt dom. GO-varianten av provenans-setningen sto allerede live som `_VEGLYS_PROVENANCE`
|
||||
siden P3; den var IKKE positivt asserted noe sted (anker-testen utelukker bare reservens setning), så
|
||||
den er nå pinnet mot fasiten.
|
||||
|
||||
**Load-bearing MÅLT mot HELE suiten (~123 s hver), fem mutasjoner alle røde + grønn kontroll:**
|
||||
ett byte i en stdout-linje (`FORSTÅ`→`FORSTA`) · detach rund-taks-dempingen i `main()` ·
|
||||
over-normaliser stderr (drop advarsels-linjene) **med fasiten regenerert under den** — begge
|
||||
likhets-testene forble GRØNNE, kun kontrollen felte den, som er hele grunnen til at kontrollen finnes ·
|
||||
literal splitt i stedet for avledet · detach frø-setningens print. **To målinger er verdt å merke:**
|
||||
byte-mutasjonen og detach-mutasjonen ble fanget av **kun** golden-testen — 800 andre tester merket
|
||||
ingenting, som er nøyaktig gapet kriterium 6 ikke dekker; og den literale splitten ble fanget av
|
||||
**kun** skille-testen (golden-testen forble grønn, siden literalen printer identisk tekst for den
|
||||
leverte bundelen).
|
||||
|
||||
**UTFØRT — punkt 5, 2 og 1 (2026-08-09, `ab7f45a`). 775 → 785 passed / 4 skipped.**
|
||||
|
||||
*Punkt 5:* to konsoll-kommandoer — `portfolio-optimiser` (`run:main`) og `portfolio-optimiser-demo`
|
||||
|
|
|
|||
|
|
@ -704,6 +704,28 @@ def _decision_line(result: RunResult) -> str:
|
|||
return f"FORKASTET — {o.reason}"
|
||||
|
||||
|
||||
def _verdict_origin_line(verdicts: Sequence[Verdict], *, marker: str, inbox_marker: str) -> str:
|
||||
"""P4 pkt. 4, the seed sentence: of Run B's previous verdicts, how many came WITH the knowledge
|
||||
base and how many the demo learned in this session.
|
||||
|
||||
Said out loud on stage, so it has to be true of the content actually being run — and the split
|
||||
is therefore DERIVED, never written down. The line above it already prints the retrieved count;
|
||||
a hand-written "one of three" would be a second copy of that number, and would go on being said
|
||||
unchanged after a bundle shipped a second seeded verdict.
|
||||
|
||||
The classifier is the two markers the demo has traced all along: a retrieved verdict carrying one
|
||||
of them is one this session produced (the Step-8 promotion, the Step-7 inbox note). That rests on
|
||||
the seeded verdict carrying NEITHER, which is measured on the delivered bundle in
|
||||
``tests/test_p4_honesty_sentences_loadbearing.py`` rather than assumed here.
|
||||
"""
|
||||
learned = sum(1 for v in verdicts if marker in v.rationale or inbox_marker in v.rationale)
|
||||
seeded = len(verdicts) - learned
|
||||
return (
|
||||
f" av disse fulgte {seeded} av {len(verdicts)} med kunnskapsbasen; "
|
||||
f"de øvrige {learned} er dem demoen lærte i denne økten"
|
||||
)
|
||||
|
||||
|
||||
def _run_trace_lines(result: RunResult, *, marker: str, marker_in_prompt: bool) -> list[str]:
|
||||
"""Steps 1-7 of one run, one labelled line per step (``method-spec`` §3), for the walkthrough.
|
||||
|
||||
|
|
@ -842,6 +864,11 @@ def main(argv: list[str] | None = None) -> int: # pragma: no cover - console tr
|
|||
print("\nKJØRING B (re-seedet kunnskapsbase + innboksen lest)")
|
||||
print(" samme åtte steg kjøres igjen; her vises kun det som ENDRET seg:")
|
||||
print(f" tidligere dommer hentet for kandidaten: {len(result.run_b.retrieved)}")
|
||||
print(
|
||||
_verdict_origin_line(
|
||||
result.run_b.retrieved, marker=result.marker, inbox_marker=result.inbox_marker
|
||||
)
|
||||
)
|
||||
print(
|
||||
f" markør '{result.marker}' (Steg 8, wiki) i hypotese-prompten: "
|
||||
f"{result.marker_in_run_b_prompt} (forventet True)"
|
||||
|
|
|
|||
4
tests/golden/demo-transcript.stderr
Normal file
4
tests/golden/demo-transcript.stderr
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<SITE-PACKAGES>/agent_framework/_skills.py:121: ExperimentalWarning: [SKILLS] SkillResource is experimental and may change or be removed in future versions without notice.
|
||||
<SITE-PACKAGES>/agent_framework/_harness/_memory.py:651: ExperimentalWarning: [HARNESS] MemoryStore is experimental and may change or be removed in future versions without notice.
|
||||
|
||||
(arbeidskopi: <TMPDIR>/po-sim-<SUFFIKS>)
|
||||
61
tests/golden/demo-transcript.stdout
Normal file
61
tests/golden/demo-transcript.stdout
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
==============================================================================
|
||||
OFFLINE SIMULERING — skriptede agent-svar, INGEN ekte modell.
|
||||
Beviser dataflyten, den deterministiske ryggraden og at læringssløyfa lukkes.
|
||||
Beviser IKKE at en levende modell ville produsert dette — forslag og dom er skriptet.
|
||||
==============================================================================
|
||||
|
||||
KUNNSKAPSBASE: veglys-fv-soer — kostbaseline erklært (ENERGI-VEGLYS-EL 4386150 x 1)
|
||||
validatorens stage 0 avstemmer forslagets kostlinjer mot disse, FØR løseren
|
||||
tallene er levert i kunnskapsbasen — utledet av fagkilder (Håndbok V124, NMFV), ikke av demo-manuset
|
||||
|
||||
KJØRING A (VEGLYS-FV-SOER — fersk kunnskapsbase, ingen tidligere dommer)
|
||||
Steg 1 — FORSTÅ KONTEKSTEN (navigert kunnskapsbase + tidligere dommer)
|
||||
navigerte konseptfiler (5): kilder-veglys-realisering.md, tiltak-adaptiv-styring.md, tiltak-led-utskifting.md, veglys-fv-soer.md, metode-ipmvp-a.md
|
||||
tidligere dommer hentet for kandidaten: 0
|
||||
markør 'realiseringsgrad=0.79' i hypotese-prompten: False
|
||||
Steg 2 — HYPOTESE (kandidat med parametere)
|
||||
tiltak: LED-utskifting av 2 500 eldre HPS-armaturer (114 W -> 70 W)
|
||||
kostlinjer: ENERGI-VEGLYS-EL 4386150 x 1
|
||||
påstått besparelse: 2100000 NOK
|
||||
Steg 3 — DEBATT (maker-checker, Group Chat)
|
||||
proposer (konvergert): {"measure":"LED-utskifting av 2 500 eldre HPS-armaturer (114 W -> 70 W)","affected_items":[…
|
||||
checker (gate på resonnementet): VERDICT=APPROVE
|
||||
Steg 4 — VALIDER / FALSIFISER (deterministisk, blokkerende)
|
||||
hypotese #1: REJECTED (claimed saving 2100000 exceeds P90 feasible 1769915)
|
||||
Steg 5 — FORBEDRE, INFORMERT OG BUNDET
|
||||
#1: grunnen fra 2100000 NOK-hypotesen mates tilbake i neste forsøk (bundet av max_attempts)
|
||||
etter forbedring: VALIDATED (påstått 445500 <= P90 1769915 NOK; tiltak: LED-utskifting av 2 500 eldre HPS-armaturer (114 W -> 70 W))
|
||||
Steg 6 — FORKAST ELLER FORESLÅ (typet utfall forlater kjøringen)
|
||||
FORESLÅTT — LED-utskifting av 2 500 eldre HPS-armaturer (114 W -> 70 W): 445500 NOK (validator=validated, checker=approve)
|
||||
Steg 7 — SVAR PÅ TILBAKEMELDING (ekspert-persona, kort løkke i kjøringen)
|
||||
dom: approved
|
||||
begrunnelse: Godkjent med realiseringskorreksjon. Den modellerte besparelsen er teknisk korrekt fra parameterne og validatoren bekrefter at den er innenfor feasibelt omraade. Men i drift realiseres erfaringsvis ~79% av en timeplan-stipulert LED-besparelse i kontorbygg (realiseringsgrad=0.79) pga. overestimerte …
|
||||
(forventet: markøren er FRAVÆRENDE her — dommen finnes ikke i wikien ennå)
|
||||
|
||||
MELLOM KJØRINGENE — to uavhengige tilbakemeldings-veier tas i bruk
|
||||
|
||||
Steg 7 (lang løkke) — EN EKSPERT LEGGER EN DOM I INNBOKSEN, ETTER KJØRINGEN
|
||||
fil: verdict-innboks/STEG7-EKSPERT-DRIFTSNOTAT.json
|
||||
bærer: realiseringsgrad=0.66 (ettersendt driftsmåling — ny kunnskap)
|
||||
rollene byttes aldri: systemet LESER denne mappa, eksperten SKRIVER den
|
||||
neste kjøring merger fila inn i minnet FØR hypotesen formes — dager kan gå
|
||||
|
||||
Steg 8 — PROMOTER GODKJENT KUNNSKAP (gatet wiki-promotering)
|
||||
skrev: promoted-verdict-b26f6501ccf74eef.md (lenket i index.md, nøytral etikett)
|
||||
bærer: realiseringsgrad=0.79 (personaens dom fra kjøring A)
|
||||
gaten er fail-closed: kun en godkjent dom promoteres — rå agent-output aldri
|
||||
|
||||
KJØRING B (re-seedet kunnskapsbase + innboksen lest)
|
||||
samme åtte steg kjøres igjen; her vises kun det som ENDRET seg:
|
||||
tidligere dommer hentet for kandidaten: 3
|
||||
av disse fulgte 1 av 3 med kunnskapsbasen; de øvrige 2 er dem demoen lærte i denne økten
|
||||
markør 'realiseringsgrad=0.79' (Steg 8, wiki) i hypotese-prompten: True (forventet True)
|
||||
markør 'realiseringsgrad=0.66' (Steg 7, innboks) i hypotese-prompten: True (forventet True)
|
||||
utfall: FORESLÅTT — LED-utskifting av 2 500 eldre HPS-armaturer (114 W -> 70 W): 445500 NOK (validator=validated, checker=approve)
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
LÆRINGSSLØYFA ER LUKKET, PÅ BEGGE TIDSSKALAER: kunnskapen eksperten godkjente i
|
||||
kjøring A nådde kjøring B's hypotese via den fil-baserte wikien (promoter ->
|
||||
re-seed -> fold), OG driftsnotatet som ble lagt i innboksen etterpå nådde den via
|
||||
fil-innboksen (skriv fil -> merge -> fold). Ingen av dem gikk gjennom minnet.
|
||||
------------------------------------------------------------------------------
|
||||
142
tests/test_golden_transcript_loadbearing.py
Normal file
142
tests/test_golden_transcript_loadbearing.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""P4 pkt. 3 — the demo transcript is pinned as a checked-in fasit, and the pin is also the
|
||||
demo's abort path.
|
||||
|
||||
**What this adds over criterion 6.** Criterion 6 is self-identity: run the demo twice, diff the two
|
||||
outputs. It catches non-determinism, and nothing else — two runs of a *regressed* demo agree with
|
||||
each other just as happily as two runs of a correct one. Between the Wednesday freeze and the
|
||||
Thursday stage there is no second run to compare against, so the fasit has to leave the process:
|
||||
the transcript is checked in, and the demo is diffed against the file.
|
||||
|
||||
**Why stdout is literal and stderr is not.** stdout was measured byte-identical across runs AND
|
||||
across a fresh clone (P4 pkt. 1), so it is pinned verbatim — the file under ``golden/`` is exactly
|
||||
what the operator sees, which is what makes it usable as the abort path: if the live run fails on
|
||||
stage, that file IS the transcript. stderr carries two spans that are environment state rather than
|
||||
program output, and both were measured to differ between a fresh clone and the working copy:
|
||||
|
||||
1. the absolute ``.../site-packages/`` prefix in the two ``ExperimentalWarning`` lines, and
|
||||
2. the temp directory behind ``(arbeidskopi: ...)`` — a fresh ``mkdtemp`` suffix every run, under a
|
||||
``TMPDIR`` that is per-user.
|
||||
|
||||
Both are masked; everything else is compared byte for byte. Pinned stderr is four lines: the two
|
||||
warnings, a blank line, and the ``arbeidskopi:`` line.
|
||||
|
||||
**The narrowness is the point, and it is under test.** P4 pkt. 2 damped the round-cap notices with a
|
||||
filter keyed on the MESSAGE precisely so that a NEW warning — from a MAF bump, or a subtree pull —
|
||||
would still reach stderr and trip this pin. A normalisation that masked whole lines would undo that
|
||||
in one step and leave a pin that can no longer fail for the reason it exists. So the masking is
|
||||
measured, not asserted: ``test_normalisation_does_not_mask_a_new_warning`` feeds a stderr with one
|
||||
extra line through the same normaliser and requires it to STOP matching the fasit.
|
||||
|
||||
**Honesty about what a golden proves.** The fasit was generated from a run, so it cannot by itself
|
||||
prove that run was right — it pins the output the P3 criteria were measured against (eight step
|
||||
lines; hypothesis #1 REJECTED on the P90 stage and the corrected one VALIDATED for the same
|
||||
candidate; both learning paths reaching Run B). Regenerating it is therefore a decision, never
|
||||
housekeeping: whoever regenerates re-takes those measurements.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_GOLDEN_DIR = Path(__file__).resolve().parent / "golden"
|
||||
_STDOUT_FASIT = _GOLDEN_DIR / "demo-transcript.stdout"
|
||||
_STDERR_FASIT = _GOLDEN_DIR / "demo-transcript.stderr"
|
||||
|
||||
# Span 1: the interpreter's site-packages prefix, up to and including the separator. Anchored on
|
||||
# the literal ``/site-packages/`` rather than on a home directory, so a checkout anywhere (or a
|
||||
# different Python minor version) normalises to the same text.
|
||||
_SITE_PACKAGES = re.compile(r"\S*/site-packages/")
|
||||
# Span 2: the throwaway working copy. The ``po-sim-`` prefix is kept VISIBLE — it is a property of
|
||||
# the program (``tempfile.mkdtemp(prefix="po-sim-")``), not of the environment — while the TMPDIR
|
||||
# root and the random suffix, which are both, are masked.
|
||||
_WORKING_COPY = re.compile(r"\(arbeidskopi: \S*/po-sim-\w+\)")
|
||||
|
||||
|
||||
def normalise_stderr(text: str) -> str:
|
||||
"""Mask the two measured environment-dependent spans, and nothing else."""
|
||||
text = _SITE_PACKAGES.sub("<SITE-PACKAGES>/", text)
|
||||
return _WORKING_COPY.sub("(arbeidskopi: <TMPDIR>/po-sim-<SUFFIKS>)", text)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def demo() -> subprocess.CompletedProcess[str]:
|
||||
"""Run the real demo once for this module.
|
||||
|
||||
Runs the ``-m`` form rather than the console script: it needs no assumption about PATH, and the
|
||||
two forms were measured to write identical stdout when the entry point was added (P4 pkt. 5).
|
||||
``PYTHONIOENCODING`` is pinned because the fasit is a file of UTF-8 bytes — without it the
|
||||
comparison would measure the operator's locale instead of the program.
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-m", "portfolio_optimiser.simulation"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return proc
|
||||
|
||||
|
||||
def test_demo_stdout_matches_the_checked_in_transcript(
|
||||
demo: subprocess.CompletedProcess[str],
|
||||
) -> None:
|
||||
"""T-P4.3a: stdout is byte-identical to the fasit — no normalisation, no tolerance.
|
||||
|
||||
RED on any change to a step line, a number, a label or the ordering. That is the whole point:
|
||||
on demo day the operator has already read this file, and anything the program says that the
|
||||
file does not is a surprise on stage.
|
||||
"""
|
||||
assert _STDOUT_FASIT.exists(), (
|
||||
f"the pinned transcript is missing: {_STDOUT_FASIT}. It is generated by re-taking the P3 "
|
||||
"measurements, not by copying whatever the demo currently prints."
|
||||
)
|
||||
assert demo.stdout == _STDOUT_FASIT.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_demo_stderr_matches_the_normalised_transcript(
|
||||
demo: subprocess.CompletedProcess[str],
|
||||
) -> None:
|
||||
"""T-P4.3b: stderr, with the two environment spans masked, is identical to the fasit.
|
||||
|
||||
RED when a new warning appears (a MAF bump, a subtree pull) and RED when the round-cap damping
|
||||
is detached — the two lines it drops would land right back in this comparison.
|
||||
"""
|
||||
assert _STDERR_FASIT.exists(), f"the pinned stderr is missing: {_STDERR_FASIT}"
|
||||
assert normalise_stderr(demo.stderr) == _STDERR_FASIT.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_normalisation_is_not_a_no_op(demo: subprocess.CompletedProcess[str]) -> None:
|
||||
"""T-P4.3c (RED-proof for the test above): the raw stderr really does differ from the fasit.
|
||||
|
||||
Without this, T-P4.3b would be indistinguishable from a run where the environment spans happened
|
||||
to be stable and the normaliser did nothing at all — and a masking that never fires cannot be
|
||||
said to be narrow.
|
||||
"""
|
||||
fasit = _STDERR_FASIT.read_text(encoding="utf-8")
|
||||
assert demo.stderr != fasit
|
||||
assert normalise_stderr(demo.stderr) == fasit
|
||||
|
||||
|
||||
def test_normalisation_does_not_mask_a_new_warning(
|
||||
demo: subprocess.CompletedProcess[str],
|
||||
) -> None:
|
||||
"""T-P4.3d (control): the masking is span-scoped, so an EXTRA stderr line still trips the pin.
|
||||
|
||||
This is the property P4 pkt. 2 paid for by keying the damping on the message rather than on the
|
||||
logger. Measured here on a synthetic stderr rather than by provoking a real warning, because the
|
||||
thing under test is the normaliser's reach — the real-warning path is what T-P4.3b covers.
|
||||
"""
|
||||
intruder = (
|
||||
"/somewhere/lib/python3.12/site-packages/agent_framework/_new.py:1: DeprecationWarning: "
|
||||
"something changed under us.\n"
|
||||
)
|
||||
polluted = intruder + demo.stderr
|
||||
assert normalise_stderr(polluted) != _STDERR_FASIT.read_text(encoding="utf-8")
|
||||
124
tests/test_p4_honesty_sentences_loadbearing.py
Normal file
124
tests/test_p4_honesty_sentences_loadbearing.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"""P4 pkt. 4 — the two honesty sentences the demo says out loud, and why one of them is DERIVED.
|
||||
|
||||
The plan pre-wrote two sentences for the stage. The first is the provenance claim about the cost
|
||||
numbers, already live as ``_VEGLYS_PROVENANCE`` since the GO (P3): the baseline the validator's
|
||||
stage 0 reconciles against was written by a domain team, not by the demo script.
|
||||
|
||||
The second is the seed sentence, about where Run B's "previous verdicts" come from. **Its pre-written
|
||||
wording is wrong against the delivered content and was corrected against measurement.** The plan said
|
||||
"one of the TWO previous verdicts in Run B came with the example — the other is the one the demo
|
||||
learned"; measured against the delivered VEGLYS bundle, Run B retrieves THREE: one seeded
|
||||
(``verdict-veglys-fro.md``), and TWO the demo produced itself — one per time scale (the Step-8
|
||||
promotion and the Step-7 inbox note). A sentence that says "two" would be a false claim made on
|
||||
stage about a number printed one line above it.
|
||||
|
||||
**Why the split is computed and not written down.** The line right above already prints the retrieved
|
||||
count. A hand-written "one of three" would be a second copy of that number in a different place —
|
||||
the exact drift the anchored-baseline discipline (P4 pkt. 0) refuses — and it would go on being said
|
||||
after a future bundle ships a second seeded verdict, silently. So the split is derived from the run,
|
||||
and the classifier is the two markers the demo has been tracing all along: a retrieved verdict that
|
||||
carries one of them is one the demo learned in this session; anything else came with the knowledge
|
||||
base.
|
||||
|
||||
That classifier rests on a property of the delivered content — the seeded verdict must carry NEITHER
|
||||
marker — so that property is measured here too, and not assumed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from portfolio_optimiser.persona import load_persona_example
|
||||
from portfolio_optimiser.simulation import (
|
||||
_INBOX_MARKER,
|
||||
_VEGLYS_PROVENANCE,
|
||||
_delivered_bundle_dir,
|
||||
_verdict_origin_line,
|
||||
)
|
||||
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict, seed_store_from_bundle
|
||||
|
||||
# The Step-8 marker is the shared expert-reviewer skill's own, read at call time — the simulation
|
||||
# takes it from the artifact rather than from a module constant, and so does this test.
|
||||
_MARKER = load_persona_example().marker
|
||||
|
||||
|
||||
def _verdict(vid: str, rationale: str) -> Verdict:
|
||||
return Verdict(
|
||||
id=vid,
|
||||
proposal_features=ProposalFeatures(
|
||||
affected_codes=frozenset({"ENERGI-VEGLYS-EL"}),
|
||||
measure_type="LED-utskifting",
|
||||
claimed_saving_nok=445500.0,
|
||||
description="LED-utskifting",
|
||||
),
|
||||
decision="approved",
|
||||
rationale=rationale,
|
||||
)
|
||||
|
||||
|
||||
def test_the_origin_line_reports_the_delivered_bundles_actual_split() -> None:
|
||||
"""T-P4.4a: three retrieved, two carrying markers -> one came with the knowledge base.
|
||||
|
||||
This is the shape the demo prints today. RED if the classification is inverted or dropped.
|
||||
"""
|
||||
retrieved = [
|
||||
_verdict("seeded", "frøsatt dom uten markør"),
|
||||
_verdict("promoted", f"persona-dom ({_MARKER})"),
|
||||
_verdict("inbox", f"driftsnotat ({_INBOX_MARKER})"),
|
||||
]
|
||||
line = _verdict_origin_line(retrieved, marker=_MARKER, inbox_marker=_INBOX_MARKER)
|
||||
assert "1" in line and "2" in line
|
||||
assert "kunnskapsbasen" in line
|
||||
|
||||
|
||||
def test_the_split_is_computed_and_not_a_written_down_constant() -> None:
|
||||
"""T-P4.4b (the distinguishing test): a bundle with a SECOND seeded verdict must move the
|
||||
number.
|
||||
|
||||
An implementation that writes "1 of 3" into the string passes T-P4.4a and fails here — which is
|
||||
the whole reason this test exists. A test that only ever sees the delivered bundle's own split
|
||||
cannot tell the two implementations apart, and would prove nothing.
|
||||
"""
|
||||
retrieved = [
|
||||
_verdict("seeded-a", "frøsatt dom uten markør"),
|
||||
_verdict("seeded-b", "en annen frøsatt dom uten markør"),
|
||||
_verdict("promoted", f"persona-dom ({_MARKER})"),
|
||||
_verdict("inbox", f"driftsnotat ({_INBOX_MARKER})"),
|
||||
]
|
||||
line = _verdict_origin_line(retrieved, marker=_MARKER, inbox_marker=_INBOX_MARKER)
|
||||
assert "2 av 4" in line, line
|
||||
|
||||
|
||||
def test_both_sentences_are_in_the_pinned_transcript() -> None:
|
||||
"""T-P4.4d: the two sentences are on the screen the operator has already read.
|
||||
|
||||
Checked against the checked-in fasit rather than by starting a fourth demo subprocess — the
|
||||
fasit is byte-compared to a live run in ``test_golden_transcript_loadbearing``, so a sentence
|
||||
that is in the file but no longer printed fails there, and a sentence dropped from BOTH fails
|
||||
here. The provenance half needs this: the anchored-reserve test only rules the reserve's
|
||||
sentence OUT, so emptying ``_VEGLYS_PROVENANCE`` would leave it green.
|
||||
"""
|
||||
fasit = (Path(__file__).resolve().parent / "golden" / "demo-transcript.stdout").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert _VEGLYS_PROVENANCE in fasit
|
||||
assert "med kunnskapsbasen; de øvrige" in fasit
|
||||
|
||||
|
||||
def test_the_delivered_seed_verdict_carries_neither_marker() -> None:
|
||||
"""T-P4.4c (RED-proof for the classifier): the property the split rests on, measured on the
|
||||
delivered bundle rather than assumed.
|
||||
|
||||
If the seeded verdict's rationale ever contained one of the two markers, it would be counted as
|
||||
something the demo learned, and the sentence on stage would overstate the loop by one. It would
|
||||
also mean the marker was present in the knowledge base from the start — which the Step-7 and
|
||||
Step-8 load-bearing tests already forbid for their own reasons, but this line depends on it too.
|
||||
"""
|
||||
store = seed_store_from_bundle(str(_delivered_bundle_dir()))
|
||||
seeded = [v for v in store.verdicts]
|
||||
assert seeded, (
|
||||
"the delivered bundle ships no seeded verdict — the classifier has nothing to see"
|
||||
)
|
||||
for verdict in seeded:
|
||||
assert _MARKER not in verdict.rationale
|
||||
assert _INBOX_MARKER not in verdict.rationale
|
||||
Loading…
Add table
Add a link
Reference in a new issue