portfolio-optimiser-claude/tests/test_persona_skill_loadbearing.py
Kjell Tore Guttormsen 22bfc80dda feat(learning): S9 — D7 læringssløyfe: verdict-inbox, fail-closed promoteringsgate, artefakt-sourced persona
- inbox.py (§4.2+§5): VerdictDocument med verbatim-id-regel; write_verdict
  authoring-primitiv (deterministisk JSON); load_inbox tolerant (skip, aldri
  raise; sortert på filnavn); merge_inbox_into_store first-write-wins,
  idempotent, skriver aldri (rolle-splitt §3 steg 7)
- promotion.py (§6): promote fail-closed mot {approved,
  approved_with_adjustment}; eksplisitt påkrevd timestamp; minimal frontmatter
  (rationale → description, aldri strukturerte læringsfelt); path-safe token
  med content-hash-fallback; idempotent index-lenking med fast nøytral label
- persona.py (§4.3): load_persona_example fail-fast (run-path-vokabular,
  marker ⊆ rationale); drop_persona_verdict artefakt-sourced ved kalltid mot
  delt shared/-artefakt
- experience.py (kirurgisk): seeding leser verdict_id VERBATIM fra frontmatter
  — re-minting ville kollidert distinkte promoterte kandidater
- 43 nye load-bearing tester (step7/step8/persona), 164/164 uten API-nøkkel;
  to-runs-bevis med fersk store + tom-inbox-kontroll; fire detach-bevis kjørt
  røde og revertert grønne

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 07:36:15 +02:00

159 lines
6.2 KiB
Python

"""Persona artifact — LOAD-BEARING (method-spec §4.3, §11).
The seam this file keeps alive: the persona judgement is sourced from the
SHARED skill artifact at call time — never from an inlined copy — and the
example stays conformant with the pipeline schema (a persona-authored verdict
must round-trip the §4.2/§5 inbox unchanged). RED when the example drifts from
the schema or when the judgement is re-inlined instead of artifact-sourced.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from pydantic import ValidationError
from portfolio_optimiser_claude.experience import (
CandidateFeatures,
VerdictStore,
fold_experience,
mint_verdict_id,
)
from portfolio_optimiser_claude.inbox import load_inbox, merge_inbox_into_store
from portfolio_optimiser_claude.persona import drop_persona_verdict, load_persona_example
SHARED_ARTIFACT = (
Path(__file__).resolve().parents[1]
/ "shared"
/ "skills"
/ "expert-reviewer"
/ "references"
/ "example-verdict.json"
)
FEATURES = CandidateFeatures(
affected_codes=frozenset({"E01"}), measure_type="led-retrofit", claimed_saving_nok=25000.0
)
def _artifact(tmp_path: Path, marker: str = "rate=0.5", rationale: str | None = None) -> Path:
path = tmp_path / "example-verdict.json"
path.write_text(
json.dumps(
{
"decision": "approved",
"marker": marker,
"rationale": rationale if rationale is not None else f"Approved; {marker} holds.",
}
),
encoding="utf-8",
)
return path
class TestSharedArtifactConformance:
"""§4.3: the DELTE artifact itself — proving the shared core is shared."""
def test_the_shared_artifact_loads_and_is_run_path_approved(self) -> None:
persona = load_persona_example(SHARED_ARTIFACT)
assert persona.decision == "approved"
def test_the_marker_is_the_traceable_payload_inside_the_rationale(self) -> None:
persona = load_persona_example(SHARED_ARTIFACT)
assert persona.marker in persona.rationale
assert persona.marker # non-empty — there must be something to trace
class TestFailFastLoading:
"""§4.3: the artifact is REQUIRED input — fail-fast, never tolerant."""
def test_missing_artifact_raises(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
load_persona_example(tmp_path / "no-such-artifact.json")
def test_malformed_json_raises(self, tmp_path: Path) -> None:
path = tmp_path / "broken.json"
path.write_text("{not json", encoding="utf-8")
with pytest.raises(ValueError):
load_persona_example(path)
def test_seed_only_decision_vocabulary_is_rejected(self, tmp_path: Path) -> None:
# §4.3: decision MUST be a run-path value — approved_with_adjustment
# exists only in seed frontmatter and the promotion gate's accepted set.
path = tmp_path / "example-verdict.json"
path.write_text(
json.dumps(
{
"decision": "approved_with_adjustment",
"marker": "m",
"rationale": "m in here",
}
),
encoding="utf-8",
)
with pytest.raises(ValidationError):
load_persona_example(path)
def test_marker_not_a_substring_of_rationale_is_rejected(self, tmp_path: Path) -> None:
path = _artifact(tmp_path, marker="rate=0.5", rationale="no payload here")
with pytest.raises(ValidationError):
load_persona_example(path)
class TestArtifactSourcedJudgement:
"""LOAD-BEARING (§11): sourced at call time — RED if the judgement is re-inlined."""
def test_editing_the_artifact_changes_the_next_dropped_verdict(self, tmp_path: Path) -> None:
artifact = _artifact(tmp_path, marker="rate=0.5")
inbox = tmp_path / "inbox"
first = drop_persona_verdict(inbox, artifact, FEATURES, description="LED retrofit")
assert "rate=0.5" in first.rationale
_artifact(tmp_path, marker="rate=0.9") # the artifact is edited in place
second = drop_persona_verdict(
inbox,
artifact,
CandidateFeatures(
affected_codes=frozenset({"E02"}),
measure_type="led-retrofit",
claimed_saving_nok=25000.0,
),
description="LED retrofit",
)
assert "rate=0.9" in second.rationale # re-inlining would freeze the judgement
class TestPipelineSchemaConformance:
"""§4.3 + §4.2: a persona-authored verdict round-trips the inbox unchanged."""
def test_dropped_verdict_is_a_conformant_inbox_file(self, tmp_path: Path) -> None:
artifact = _artifact(tmp_path)
inbox = tmp_path / "inbox"
dropped = drop_persona_verdict(inbox, artifact, FEATURES, description="LED retrofit")
assert dropped.id == mint_verdict_id(FEATURES)
assert (inbox / f"{dropped.id}.json").is_file()
(loaded,) = load_inbox(inbox)
assert loaded == dropped
def test_the_persona_marker_reaches_a_later_prompt_via_the_inbox(self, tmp_path: Path) -> None:
# Persona → authoring primitive → tolerant load → merge → fold: the
# traceable payload follows the persona's judgement into the prompt.
artifact = _artifact(tmp_path, marker="rate=0.77")
inbox = tmp_path / "inbox"
drop_persona_verdict(inbox, artifact, FEATURES, description="LED retrofit")
store = VerdictStore()
merge_inbox_into_store(store, inbox)
prompt = fold_experience(store, FEATURES, "Base task.", k=3)
assert "rate=0.77" in prompt
def test_the_shared_artifact_feeds_the_pipeline_directly(self, tmp_path: Path) -> None:
# The canonical example itself flows end-to-end — schema drift in the
# shared artifact turns this RED.
inbox = tmp_path / "inbox"
drop_persona_verdict(inbox, SHARED_ARTIFACT, FEATURES, description="LED retrofit")
persona = load_persona_example(SHARED_ARTIFACT)
store = VerdictStore()
merge_inbox_into_store(store, inbox)
prompt = fold_experience(store, FEATURES, "Base task.", k=3)
assert persona.marker in prompt