feat(s36): estimate table (kost-mot-verdi + placeholder rows + sourced quality guidance)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 10:02:00 +02:00
commit 37625435c4
3 changed files with 150 additions and 0 deletions

View file

@ -76,3 +76,17 @@ def test_unknown_effort_raises() -> None:
pricing = costsim.PricingContract(**_VALID_PRICING)
with pytest.raises(ValueError, match="unknown effort"):
costsim.estimate_run_ore("m-cheap", "turbo", pricing)
def test_estimate_table_structure() -> None:
pricing = costsim.PricingContract(**_VALID_PRICING)
mm = {"prof": {"proposer": "m-cheap", "checker": "m-dear"}}
table = costsim.build_estimate_table(
"prof", ["low", "high"], pricing, n_projects=2, model_map=mm
)
assert len(table["rows"]) == 4 # 2 roles × 2 efforts
for row in table["rows"]:
assert row["estimat_type"] == "øvre grense"
assert isinstance(row["estimat_kr"], str) # edge-formatted string, never a float
assert table["kost_mot_verdi"]["kjoring_kostet_ore"] > 0
assert table["pricing_source"] == "unit-test prices"

View file

@ -125,3 +125,46 @@ def test_estimate_scales_with_model_and_effort() -> None:
assert dear_high != high, "model must scale the estimate"
# control: same model + same effort → identical (not incidental)
assert costsim.estimate_run_ore("m-cheap", "high", pricing) == high
def test_azure_placeholder_rows_unpriced() -> None:
"""The defined azure path: ``--profile azure`` ships REPLACE-WITH-* placeholders → rows are
unpriced (``estimat_ore`` None, placeholder label), NEVER a crash. Uses the bundled config."""
pricing = costsim.load_pricing()
table = costsim.build_estimate_table("azure", ["standard"], pricing, n_projects=4)
assert table["rows"], "azure profile must still produce rows"
for row in table["rows"]:
assert row["estimat_ore"] is None
assert "placeholder" in row["estimat_type"]
def test_table_carries_kost_mot_verdi_field() -> None:
"""SC5: the table carries a top-level ``kost_mot_verdi`` field ready for the S5.4 value report.
Detach point: remove the field from ``build_estimate_table`` RED."""
pricing = costsim.PricingContract(**_TWO_MODEL_PRICING)
table = costsim.build_estimate_table(
"p", ["standard"], pricing, n_projects=1, model_map={"p": {"proposer": "m-cheap"}}
)
assert "kost_mot_verdi" in table
assert "kjoring_kostet_ore" in table["kost_mot_verdi"]
def test_row_carries_sourced_quality_guidance() -> None:
"""Brief Goal 4: a priced row surfaces ``kvalitets_veiledning`` with a source (guidance with
kilde, never a bare number). Detach point: drop ``kvalitets_veiledning`` from the row RED."""
pricing = costsim.PricingContract(**_PRICING) # m-known carries guidance
table = costsim.build_estimate_table(
"p", ["standard"], pricing, n_projects=1, model_map={"p": {"proposer": "m-known"}}
)
row = table["rows"][0]
assert row["kvalitets_veiledning"] is not None
assert row["kvalitets_veiledning"]["source"].strip()
def test_table_is_byte_deterministic() -> None:
"""Serialized table is byte-stable across identical inputs (sort_keys/indent/LF)."""
pricing = costsim.PricingContract(**_TWO_MODEL_PRICING)
mm = {"p": {"proposer": "m-cheap", "checker": "m-dear"}}
t1 = costsim.build_estimate_table("p", ["low", "high"], pricing, n_projects=3, model_map=mm)
t2 = costsim.build_estimate_table("p", ["low", "high"], pricing, n_projects=3, model_map=mm)
assert costsim.dump_estimate_table(t1).encode() == costsim.dump_estimate_table(t2).encode()