"""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