Model/tests/domain/epc/test_glazing_mix_guard.py
Jun-te Kim 787f0066c8 Dominant secondary-glazing split resolves deterministically to SECONDARY 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:09:47 +00:00

106 lines
3.9 KiB
Python

from __future__ import annotations
import pytest
from domain.epc.property_overrides.glazing_type import GlazingType
from domain.epc.property_overrides.glazing_mix_guard import glazing_mix_guard
def test_a_genuine_percentage_mix_classifies_as_mixed() -> None:
# Arrange — the bug: an aggregate mix the LLM would flatten to "Double".
description = "40% double glazing 2002 or later, 60% single glazing"
# Act
result = glazing_mix_guard(description)
# Assert — a mix defers to the cert's per-window glazing, not a whole-dwelling
# type.
assert result is GlazingType.MIXED
@pytest.mark.parametrize(
"description",
[
"4% double glazing 2002 or later, 96% single glazing",
"10% double glazing 2002 or later, 90% single glazing", # exactly at threshold
"5% double glazing pre-2002, 95% single glazing",
],
)
def test_dominant_single_glazing_resolves_to_single(description: str) -> None:
# The bug: the LLM flattened a >= 90%-single dwelling onto its *minority* double
# type. Single glazing is era-free, so a dominant-single split is fully
# determined — the guard must claim it (SINGLE) rather than defer to the LLM.
# Act
result = glazing_mix_guard(description)
# Assert
assert result is GlazingType.SINGLE
@pytest.mark.parametrize(
("description", "expected"),
[
# >= 90% double with a *stated* era is era-pinned — the LLM otherwise
# flattens it onto the 4% minority single (the symmetric bug to the
# dominant-single case above).
("96% double glazing 2002 or later, 4% single glazing", GlazingType.DOUBLE_POST_2002),
("95% double glazing pre-2002, 5% single glazing", GlazingType.DOUBLE_PRE_2002),
("90% double glazing 2002 or later, 10% single glazing", GlazingType.DOUBLE_POST_2002),
# Triple is era-bearing too — resolve when the era is stated.
("92% triple glazing pre-2002, 8% single glazing", GlazingType.TRIPLE_PRE_2002),
("91% triple glazing 2002 or later, 9% single glazing", GlazingType.TRIPLE_POST_2002),
],
)
def test_dominant_era_stated_double_or_triple_resolves_deterministically(
description: str, expected: GlazingType
) -> None:
# Act
result = glazing_mix_guard(description)
# Assert — a dominant double/triple whose era is stated is fully determined, so
# the guard claims it rather than trusting the LLM.
assert result is expected
@pytest.mark.parametrize(
"description",
[
"95% secondary glazing (sap 9.94), 5% single glazing",
"90% secondary glazing, 10% double glazing 2002 or later",
],
)
def test_dominant_secondary_glazing_resolves_to_secondary(description: str) -> None:
# Secondary glazing is era-free like single (SAP10 code 5, U = 2.9 regardless
# of install year), so a dominant-secondary split is fully determined — the
# guard must claim it rather than let the LLM flatten it onto the minority
# type (Model#1416).
# Act
result = glazing_mix_guard(description)
# Assert
assert result is GlazingType.SECONDARY
@pytest.mark.parametrize(
"description",
[
"100% double glazing 2002 or later", # uniform — one type
"100% secondary glazing (sap 9.94)", # uniform — one type
"90% double glazing unknown age, 10% single glazing", # dominant but era unstated
"80% double glazing 2002 or later, 20% double glazing pre-2002", # same base type
"some double glazing and a bit of single", # unparseable — no percentages
"",
],
)
def test_uniform_or_ambiguous_glazing_defers_to_the_llm(description: str) -> None:
# The guard only claims a genuine mix or a fully-determined dominant split. A
# uniform assertion, a dominant split whose era is unstated ("unknown age" — the
# era is genuinely ambiguous), or a varied phrasing is left for the LLM.
# Act
result = glazing_mix_guard(description)
# Assert
assert result is None