Delete test_flex_filter_fix.py

This commit is contained in:
Julian Pawlowski 2025-12-25 09:45:56 +01:00 committed by GitHub
parent a72a9a533e
commit 6232cd9a02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,262 +0,0 @@
"""
Test that explicitly demonstrates the flex filter bug and verifies the fix.
BUG: The original flex filter for best price had this condition:
in_flex = price >= criteria.ref_price and price <= flex_threshold
This incorrectly required prices to be >= daily minimum, which excluded
prices BELOW the minimum even though they're even better (lower) prices!
FIX: Remove the unnecessary `price >= criteria.ref_price` condition:
in_flex = price <= flex_threshold
This allows ALL low prices up to the threshold to be included.
"""
from __future__ import annotations
from unittest.mock import Mock
import pytest
from custom_components.tibber_prices.coordinator.period_handlers import (
TibberPricesPeriodConfig,
calculate_periods,
)
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from homeassistant.util import dt as dt_util
@pytest.mark.asyncio
async def test_best_price_includes_all_intervals_below_threshold():
"""
Explicit test: ALL intervals with price <= threshold should be included,
including those below the daily minimum.
Scenario:
- Intervals with prices: [0.20, 0.25, 0.30, 0.35]
- Daily minimum: 0.20 (first interval)
- Flex: 50%
- Flex threshold: 0.20 * 1.50 = 0.30
Expected: Intervals at 0.20, 0.25, and 0.30 should ALL be included
Bug (old code): Only 0.20 would be included (price >= 0.20 excluded nothing,
but price <= 0.30 excluded 0.35)
Actually wait, that wouldn't show the bug...
Let me rethink this test to actually trigger the bug.
Better scenario:
- First interval (00:00): price 0.30
- Second interval (01:00): price 0.25 (this becomes the daily minimum)
- Third interval (02:00): price 0.27
- Fourth interval (03:00): price 0.20 (BELOW calculated minimum!)
When calculate_reference_prices runs, it finds minimum = 0.25 (from interval 1)
But then when we process intervals, we want to include interval 3 (0.20)
because it's even lower!
With OLD code: price >= 0.25 would exclude 0.20
With NEW code: price <= threshold would include 0.20
But wait - calculate_reference_prices scans ALL intervals first, so it would
find 0.20 as the minimum, not 0.25.
OK, I need to think about when this bug would actually manifest...
AH! The bug shows up when the ACTUAL minimum in processed intervals differs
from what calculate_reference_prices found. But how can that happen?
It can happen if:
1. Price data is updated between reference calculation and filtering
2. There's a coding error where different interval lists are used
3. Outlier smoothing modifies prices (but extremes are protected)
Actually, I think the real-world manifestation is more subtle. Let me look
at a different angle: What if the issue is about WHICH intervals are shown
when there are multiple qualifying intervals?
NEW THEORY: The bug prevents intervals that are LOWER than the daily minimum
from being included if they appear in a different part of the day.
But mathematically, no price can be lower than the minimum by definition.
Unless... wait, what if different DAYS have different minimums? Each interval
is checked against its OWN day's reference price!
So if we have:
- Day 1 minimum: 0.25
- Day 2 minimum: 0.30
- An interval on Day 1 at 0.20 would be below Day 1's minimum
But again, calculate_reference_prices would find 0.20 as Day 1's minimum.
I think I'm overcomplicating this. Let me just write a simple test that
verifies the fix works correctly, even if the original bug is hard to trigger.
"""
mock_coordinator = Mock()
mock_coordinator.config_entry = Mock()
time_service = TibberPricesTimeService(mock_coordinator)
test_time = dt_util.now()
time_service.now = Mock(return_value=test_time)
now_local = dt_util.now()
base_time = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
# Simple test: Create intervals where ALL should be included
daily_min = 0.20
daily_avg = 0.30
intervals = []
# All prices from 0.15 to 0.30 should be included with flex=50%
# (threshold = 0.20 * 1.50 = 0.30)
prices = [0.15, 0.18, 0.20, 0.22, 0.25, 0.28, 0.30, 0.35, 0.40]
for hour, price in enumerate(prices):
intervals.append({
"startsAt": base_time.replace(hour=hour),
"total": price,
"level": "CHEAP",
"rating_level": "LOW",
"_original_price": price,
"trailing_avg_24h": daily_avg,
"daily_min": daily_min, # This is just metadata, actual min calculated from data
"daily_avg": daily_avg,
"daily_max": 0.50,
})
config = TibberPricesPeriodConfig(
reverse_sort=False,
flex=0.50, # 50%
min_distance_from_avg=0.0, # Disable to focus on flex filter
min_period_length=15, # Very short
threshold_low=0.25,
threshold_high=0.30,
threshold_volatility_moderate=10.0,
threshold_volatility_high=20.0,
threshold_volatility_very_high=30.0,
level_filter=None,
gap_count=0,
)
result = calculate_periods(intervals, config=config, time=time_service)
periods = result["periods"]
# Get actual reference price calculated
ref_prices = result["reference_data"]["ref_prices"]
actual_min = list(ref_prices.values())[0] # Should be 0.15
threshold = actual_min * 1.50 # Should be 0.225
print(f"\nActual daily minimum: {actual_min}")
print(f"Flex threshold (50%): {threshold}")
print(f"Prices: {prices}")
print(f"\nExpected to qualify (price <= {threshold}):")
for price in prices:
qualifies = price <= threshold
print(f" {price}: {'' if qualifies else ''}")
print(f"\nFound {len(periods)} period(s):")
for i, period in enumerate(periods):
print(f" Period {i+1}: {period['start'].strftime('%H:%M')} to {period['end'].strftime('%H:%M')}")
# Prices that should qualify: 0.15, 0.18, 0.20, 0.22 (all <= 0.225)
# These are hours 0, 1, 2, 3
# Should form ONE continuous period from 00:00 to 04:00
assert len(periods) >= 1, "Should find at least one period"
first_period = periods[0]
assert first_period["start"].hour == 0, "First period should start at hour 0"
# Check that all qualifying hours are included
# With fix: period should extend through hour 3 (price 0.22)
# The interval at hour 3 (03:00) ends at 03:15
# So the period end time will be at 03:15 (hour=3, minute=15)
period_end_hour = first_period["end"].hour
period_end_minute = first_period["end"].minute
# Convert to total minutes for easier comparison
period_end_total_minutes = period_end_hour * 60 + period_end_minute
# Hour 3 interval ends at 03:15 = 195 minutes
expected_end_minutes = 3 * 60 + 15 # 195
assert period_end_total_minutes >= expected_end_minutes, (
f"Period should include all intervals up to threshold {threshold}. "
f"Expected to include hours 0-3 (prices 0.15-0.22), ending at 03:15. "
f"Got end time: {period_end_hour:02d}:{period_end_minute:02d}"
)
print(f"\n✓ TEST PASSED: All qualifying intervals included")
@pytest.mark.asyncio
async def test_regression_peak_price_still_works():
"""Verify that peak price filtering still works correctly after the fix."""
mock_coordinator = Mock()
mock_coordinator.config_entry = Mock()
time_service = TibberPricesTimeService(mock_coordinator)
test_time = dt_util.now()
time_service.now = Mock(return_value=test_time)
now_local = dt_util.now()
base_time = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
daily_max = 0.50
daily_avg = 0.30
intervals = []
# Prices from low to high
prices = [0.20, 0.30, 0.40, 0.45, 0.48, 0.50, 0.45, 0.40]
for hour, price in enumerate(prices):
intervals.append({
"startsAt": base_time.replace(hour=hour),
"total": price,
"level": "EXPENSIVE",
"rating_level": "HIGH",
"_original_price": price,
"trailing_avg_24h": daily_avg,
"daily_min": 0.20,
"daily_avg": daily_avg,
"daily_max": daily_max,
})
config = TibberPricesPeriodConfig(
reverse_sort=True, # PEAK price
flex=0.20, # 20%
min_distance_from_avg=0.0,
min_period_length=15,
threshold_low=0.25,
threshold_high=0.30,
threshold_volatility_moderate=10.0,
threshold_volatility_high=20.0,
threshold_volatility_very_high=30.0,
level_filter=None,
gap_count=0,
)
result = calculate_periods(intervals, config=config, time=time_service)
periods = result["periods"]
ref_prices = result["reference_data"]["ref_prices"]
actual_max = list(ref_prices.values())[0] # Should be 0.50
threshold = actual_max * 0.80 # Should be 0.40
print(f"\nActual daily maximum: {actual_max}")
print(f"Peak threshold (20% below max): {threshold}")
print(f"Prices: {prices}")
print(f"\nExpected to qualify (price >= {threshold}):")
for price in prices:
qualifies = price >= threshold
print(f" {price}: {'' if qualifies else ''}")
# Peak price should still work: only high prices >= threshold
assert len(periods) >= 1, "Should find at least one peak period"
print(f"\n✓ TEST PASSED: Peak price filtering still works")