portfolio-optimiser-claude/tests/test_step5_refine_loadbearing.py
Kjell Tore Guttormsen 30ba68a703 test(loadbearing): close the vacuous-negative class across the whole suite
Oekt 17 found the class on four named files. This sweep ENUMERATES it: 42 negative
substring assertions across 21 test files (STATE's "~34 across 23" was a premise --
measured, it is 42/21). Sixteen of them measured an absence without ever having
shown presence; all sixteen now carry a positive control asserting the searched-for
string PRESENT in the source artifact, in EXACTLY the form the negative looks for.

Files touched: test_costsim, test_loop, test_okf (3 sites), test_preflight,
test_run_entrance, test_s10_run_layer, test_sdk_version_guard, test_simulation
(2 sites), test_step1_expel, test_step5_refine, test_step7_async_loop,
test_step8_promotion, test_valuereport.

VALUE-PROOF (green-without / red-with, per the oekt-17 rule that a detach proof is
not a value proof). Seven source/fixture mutations, each making the negative vacuous:

  M1 verdict fixture loses the realization signal        VALUE-PROVEN
  M2 decoy fixture loses its text                        VALUE-PROVEN
  M3 renderer stops emitting typed section headings      VALUE-PROVEN
  M4 promotion stops writing the marker                  VALUE-PROVEN (pass 2)
  M5 fold stops rendering the realization surface        VALUE-PROVEN
  M6 report stops labelling the cost section             VALUE-PROVEN
  M7 preflight stops importing the SDK                   VALUE-PROVEN

M4 needed pass 2: a PRECEDING assertion caught the same mutation, hiding the new
control behind it -- the oekt-17 lesson reproduced. The remaining nine controls are
vacuity guards (non-emptiness / form-presence) whose mutation would have to break
the source artificially; they are stated as guards, not claimed as value-proven.

MEASURED FINDING (test_loop): the FIRST-RUN-MARKER negative cannot be given a
positive control at all. Within a run only the CHECKER's critique is fed back --
the proposer's own prior reasoning crosses no prompt boundary, not even within a
run. So that negative holds trivially. Left in place with the limitation stated in
the test rather than dressed up as a controlled seam; the CRITIQUE negative beside
it IS controlled and is the real seam.

Mutations were in-place on src/ and shared/ with original bytes restored and
sha-verified; git status clean before and after. Suite 688 -> 688 (assertions added
inside existing tests, no new test cases). ruff + mypy --strict green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Vc5PmZGjwuJypdhzKnJa5
2026-07-31 21:39:28 +02:00

123 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""Load-bearing: informed, bounded refinement (method-spec §3 Step 5, §11).
RED-conditions proved here: the prior rejection reason no longer appears
VERBATIM in the next prompt, or the outcome never flips. The flip proof uses a
prompt-SENSITIVE scripted stand-in (honesty rule §1): it returns a feasible
proposal ONLY when the full rejection reason is present in the prompt — so the
test goes red the moment the informed block is detached. Bounds proved: only
the MOST RECENT reason is carried (never accumulated history), never the prior
proposal JSON, attempt 1 uses the unchanged base prompt, and the loop stops at
``max_attempts``.
"""
from __future__ import annotations
import json
import pytest
from _scripted import ScriptedClient, reply
from portfolio_optimiser_claude.budget import BudgetMeter
from portfolio_optimiser_claude.contracts import TerminationContract
from portfolio_optimiser_claude.ir import SavingsProposal
from portfolio_optimiser_claude.loop import ModelReply, run_candidate_loop
from portfolio_optimiser_claude.validator import Rejection, ValidatedProposal, validate_proposal
# 100 × 1000 = 100_000 NOK total; nominal feasible 30_000 == p90 (no band).
GOOD = {
"project_id": "p1",
"measure": "led-retrofit",
"affected_items": [{"code": "E01", "quantity": 100, "unit_cost": 1000}],
"claimed_saving_nok": 25000,
}
BAD_90K = dict(GOOD, claimed_saving_nok=90000) # IR-valid, above p90 → rejected
BAD_80K = dict(GOOD, claimed_saving_nok=80000) # a SECOND distinct rejection reason
def _rejection_reason(raw: dict[str, object]) -> str:
outcome = validate_proposal(SavingsProposal.model_validate(raw))
assert isinstance(outcome, Rejection)
return outcome.reason
def _meter() -> BudgetMeter:
return BudgetMeter(TerminationContract(max_rounds=50, max_tokens=10_000))
class TestInformedRefinement:
def test_the_outcome_flips_because_the_reason_reaches_the_prompt(self) -> None:
# THE load-bearing flip: the stand-in addresses the falsification ONLY
# if the full rejection reason appears verbatim — red at detach.
reason_90k = _rejection_reason(BAD_90K)
def script(role: str, prompt: str) -> ModelReply:
if reason_90k in prompt:
return reply(json.dumps(GOOD))
return reply(json.dumps(BAD_90K))
client = ScriptedClient(script=script)
result = run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=3)
assert isinstance(result.outcome, ValidatedProposal)
assert result.attempts == 2
assert reason_90k in client.prompts("proposer")[1]
def test_attempt_one_uses_the_unchanged_base_prompt(self) -> None:
client = ScriptedClient(replies=[reply(json.dumps(GOOD))])
run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=3)
assert client.prompts("proposer")[0] == "base prompt"
def test_only_the_most_recent_reason_is_carried(self) -> None:
# §3 Step 5: never an accumulated history — bounded prompt growth.
reason_90k = _rejection_reason(BAD_90K)
reason_80k = _rejection_reason(BAD_80K)
client = ScriptedClient(
replies=[
reply(json.dumps(BAD_90K)),
reply(json.dumps(BAD_80K)),
reply(json.dumps(GOOD)),
]
)
result = run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=3)
assert isinstance(result.outcome, ValidatedProposal)
third_prompt = client.prompts("proposer")[2]
assert reason_80k in third_prompt
assert reason_90k not in third_prompt
def test_the_prior_proposal_json_is_never_carried(self) -> None:
# The model must address the falsification, not parrot the rejected
# candidate — only the REASON crosses attempts.
client = ScriptedClient(replies=[reply(json.dumps(BAD_90K)), reply(json.dumps(GOOD))])
run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=3)
second_prompt = client.prompts("proposer")[1]
# Positive control: the REASON did cross into the retry prompt — so the two
# negatives measure what was deliberately withheld, not an empty or misindexed
# prompt that would carry nothing either way.
assert _rejection_reason(BAD_90K) in second_prompt
assert json.dumps(BAD_90K) not in second_prompt
assert '"affected_items"' not in second_prompt
def test_the_loop_stops_at_max_attempts(self) -> None:
client = ScriptedClient(script=lambda role, prompt: reply(json.dumps(BAD_90K)))
result = run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=3)
assert isinstance(result.outcome, Rejection)
assert result.attempts == 3
assert len(client.prompts("proposer")) == 3
def test_max_attempts_must_be_positive(self) -> None:
client = ScriptedClient(replies=[reply(json.dumps(GOOD))])
with pytest.raises(ValueError):
run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=0)
def test_round_ticks_are_charged_between_attempts(self) -> None:
client = ScriptedClient(replies=[reply(json.dumps(BAD_90K)), reply(json.dumps(GOOD))])
meter = _meter()
run_candidate_loop(client, "base prompt", meter=meter, max_attempts=3)
assert meter.rounds_used == 1
def test_a_never_validating_run_yields_a_typed_rejection_not_a_bare_failure(self) -> None:
# §3 Step 6: the outcome is either the validated proposal or a TYPED
# rejection with its reason — never a bare failure.
client = ScriptedClient(script=lambda role, prompt: reply(json.dumps(BAD_90K)))
result = run_candidate_loop(client, "base prompt", meter=_meter(), max_attempts=2)
assert isinstance(result.outcome, Rejection)
assert "exceeds the optimistic feasible bound" in result.outcome.reason