- Updated `extract_value_raw` to improve JSON parsing and handle unavailable data more effectively. - Introduced new functions in `placeholder_resolver.py` for standardized responses when data is unavailable, enhancing clarity for users and AI. - Modified various data retrieval functions to utilize the new response format, providing detailed reasons for unavailability. - Improved availability checks in `export_placeholder_values_extended` to account for new response formats. These changes enhance the robustness of the placeholder system and improve user experience by providing clearer error messages and data handling.
318 lines
11 KiB
Python
318 lines
11 KiB
Python
"""
|
|
Tests for Enhanced Placeholder Metadata System V2
|
|
|
|
Tests the strict quality controls and enhanced extraction logic.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import pytest
|
|
from placeholder_metadata import (
|
|
PlaceholderType,
|
|
TimeWindow,
|
|
OutputType
|
|
)
|
|
from placeholder_metadata_enhanced import (
|
|
extract_value_raw,
|
|
infer_unit_strict,
|
|
detect_time_window_precise,
|
|
resolve_real_source,
|
|
create_activity_quality_policy,
|
|
calculate_completeness_score
|
|
)
|
|
|
|
|
|
# ── Value Raw Extraction Tests ────────────────────────────────────────────────
|
|
|
|
def test_value_raw_json():
|
|
"""JSON outputs must return actual JSON objects."""
|
|
# Valid JSON
|
|
val, success = extract_value_raw('{"goals": [1,2,3]}', OutputType.JSON, PlaceholderType.RAW_DATA)
|
|
assert success
|
|
assert isinstance(val, dict)
|
|
assert val == {"goals": [1,2,3]}
|
|
|
|
# JSON array
|
|
val, success = extract_value_raw('[1, 2, 3]', OutputType.JSON, PlaceholderType.RAW_DATA)
|
|
assert success
|
|
assert isinstance(val, list)
|
|
|
|
# Invalid JSON
|
|
val, success = extract_value_raw('not json', OutputType.JSON, PlaceholderType.RAW_DATA)
|
|
assert not success
|
|
assert val is None
|
|
|
|
# Resolver-Fehlerhülle (kein verwertbares JSON für Charts)
|
|
val, success = extract_value_raw(
|
|
'{"_available": false, "_reason": "test"}',
|
|
OutputType.JSON,
|
|
PlaceholderType.RAW_DATA,
|
|
)
|
|
assert not success
|
|
assert val is None
|
|
|
|
|
|
def test_value_raw_number():
|
|
"""Numeric outputs must extract numbers without units."""
|
|
# Number with unit
|
|
val, success = extract_value_raw('85.8 kg', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert success
|
|
assert val == 85.8
|
|
|
|
# Integer
|
|
val, success = extract_value_raw('42 Jahre', OutputType.INTEGER, PlaceholderType.ATOMIC)
|
|
assert success
|
|
assert val == 42
|
|
|
|
# Negative number
|
|
val, success = extract_value_raw('-12.5 kg', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert success
|
|
assert val == -12.5
|
|
|
|
# No number
|
|
val, success = extract_value_raw('nicht verfügbar', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert not success
|
|
|
|
val, success = extract_value_raw(
|
|
'nicht verfügbar — Keine Messungen (detail)',
|
|
OutputType.NUMBER,
|
|
PlaceholderType.ATOMIC,
|
|
)
|
|
assert not success
|
|
|
|
|
|
def test_value_raw_markdown():
|
|
"""Markdown outputs keep as string."""
|
|
val, success = extract_value_raw('# Heading\nText', OutputType.MARKDOWN, PlaceholderType.RAW_DATA)
|
|
assert success
|
|
assert val == '# Heading\nText'
|
|
|
|
|
|
def test_value_raw_date():
|
|
"""Date outputs prefer ISO format."""
|
|
# ISO format
|
|
val, success = extract_value_raw('2026-03-29', OutputType.DATE, PlaceholderType.ATOMIC)
|
|
assert success
|
|
assert val == '2026-03-29'
|
|
|
|
# Non-ISO (still accepts but marks as uncertain)
|
|
val, success = extract_value_raw('29.03.2026', OutputType.DATE, PlaceholderType.ATOMIC)
|
|
assert not success # Unknown format
|
|
|
|
|
|
# ── Unit Inference Tests ──────────────────────────────────────────────────────
|
|
|
|
def test_unit_no_units_for_scores():
|
|
"""Scores are dimensionless (0-100 scale), no units."""
|
|
unit = infer_unit_strict('goal_progress_score', 'Progress score', OutputType.INTEGER, PlaceholderType.ATOMIC)
|
|
assert unit is None
|
|
|
|
unit = infer_unit_strict('protein_adequacy_28d', 'Protein adequacy', OutputType.INTEGER, PlaceholderType.ATOMIC)
|
|
assert unit is None
|
|
|
|
|
|
def test_unit_no_units_for_correlations():
|
|
"""Correlations are dimensionless."""
|
|
unit = infer_unit_strict('correlation_energy_weight', 'Correlation', OutputType.JSON, PlaceholderType.INTERPRETED)
|
|
assert unit is None
|
|
|
|
|
|
def test_unit_no_units_for_ratios():
|
|
"""Ratios and percentages are dimensionless."""
|
|
unit = infer_unit_strict('waist_hip_ratio', 'Waist-hip ratio', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert unit is None
|
|
|
|
unit = infer_unit_strict('quality_sessions_pct', 'Quality sessions percentage', OutputType.INTEGER, PlaceholderType.ATOMIC)
|
|
assert unit is None
|
|
|
|
|
|
def test_unit_correct_units_for_measurements():
|
|
"""Physical measurements have correct units."""
|
|
# Weight
|
|
unit = infer_unit_strict('weight_aktuell', 'Aktuelles Gewicht', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert unit == 'kg'
|
|
|
|
# Circumference
|
|
unit = infer_unit_strict('waist_28d_delta', 'Taillenumfang', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert unit == 'cm'
|
|
|
|
# Heart rate
|
|
unit = infer_unit_strict('vitals_avg_hr', 'Ruhepuls', OutputType.INTEGER, PlaceholderType.ATOMIC)
|
|
assert unit == 'bpm'
|
|
|
|
# HRV
|
|
unit = infer_unit_strict('vitals_avg_hrv', 'HRV', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
|
assert unit == 'ms'
|
|
|
|
|
|
def test_unit_no_units_for_json():
|
|
"""JSON outputs never have units."""
|
|
unit = infer_unit_strict('active_goals_json', 'Active goals', OutputType.JSON, PlaceholderType.RAW_DATA)
|
|
assert unit is None
|
|
|
|
|
|
# ── Time Window Detection Tests ───────────────────────────────────────────────
|
|
|
|
def test_time_window_explicit_suffix():
|
|
"""Explicit suffixes are most reliable."""
|
|
tw, certain, mismatch = detect_time_window_precise('weight_7d_median', '', '', '')
|
|
assert tw == TimeWindow.DAYS_7
|
|
assert certain == True
|
|
|
|
tw, certain, mismatch = detect_time_window_precise('protein_avg_28d', '', '', '')
|
|
assert tw == TimeWindow.DAYS_28
|
|
assert certain == True
|
|
|
|
|
|
def test_time_window_latest():
|
|
"""Latest/current keywords."""
|
|
tw, certain, mismatch = detect_time_window_precise('weight_aktuell', 'Aktuelles Gewicht', '', '')
|
|
assert tw == TimeWindow.LATEST
|
|
assert certain == True
|
|
|
|
|
|
def test_time_window_from_contract():
|
|
"""Time window from semantic contract."""
|
|
contract = 'Berechnet aus weight_log über 7 Tage'
|
|
tw, certain, mismatch = detect_time_window_precise('weight_avg', '', '', contract)
|
|
assert tw == TimeWindow.DAYS_7
|
|
assert certain == True
|
|
|
|
|
|
def test_time_window_legacy_mismatch():
|
|
"""Detect legacy description mismatch."""
|
|
description = 'Durchschnitt 30 Tage'
|
|
contract = 'Berechnet über 7 Tage'
|
|
|
|
tw, certain, mismatch = detect_time_window_precise('weight_avg', description, '', contract)
|
|
assert tw == TimeWindow.DAYS_7 # Implementation wins
|
|
assert mismatch is not None
|
|
|
|
|
|
def test_time_window_unknown():
|
|
"""Returns unknown if cannot determine."""
|
|
tw, certain, mismatch = detect_time_window_precise('some_metric', '', '', '')
|
|
assert tw == TimeWindow.UNKNOWN
|
|
assert certain == False
|
|
|
|
|
|
# ── Source Provenance Tests ───────────────────────────────────────────────────
|
|
|
|
def test_source_skip_safe_wrappers():
|
|
"""Safe wrappers are not real sources."""
|
|
func, module, tables, kind = resolve_real_source('_safe_int')
|
|
assert func is None
|
|
assert module is None
|
|
assert kind == "wrapper"
|
|
|
|
|
|
def test_source_real_data_layer():
|
|
"""Real data layer sources."""
|
|
func, module, tables, kind = resolve_real_source('get_latest_weight')
|
|
assert func == 'get_latest_weight_data'
|
|
assert module == 'body_metrics'
|
|
assert 'weight_log' in tables
|
|
assert kind == 'direct'
|
|
|
|
|
|
def test_source_computed():
|
|
"""Computed sources."""
|
|
func, module, tables, kind = resolve_real_source('calculate_bmi')
|
|
assert 'weight_log' in tables
|
|
assert 'profiles' in tables
|
|
assert kind == 'computed'
|
|
|
|
|
|
def test_source_aggregated():
|
|
"""Aggregated sources."""
|
|
func, module, tables, kind = resolve_real_source('get_nutrition_avg')
|
|
assert func == 'get_nutrition_average_data'
|
|
assert module == 'nutrition_metrics'
|
|
assert kind == 'aggregated'
|
|
|
|
|
|
# ── Quality Filter Policy Tests ───────────────────────────────────────────────
|
|
|
|
def test_quality_filter_for_activity():
|
|
"""Activity placeholders need quality filter policies."""
|
|
policy = create_activity_quality_policy('activity_summary')
|
|
assert policy is not None
|
|
assert policy.enabled == True
|
|
assert policy.default_filter_level == "quality"
|
|
assert policy.null_quality_handling == "exclude"
|
|
assert policy.includes_poor == False
|
|
|
|
|
|
def test_quality_filter_not_for_non_activity():
|
|
"""Non-activity placeholders don't need quality filters."""
|
|
policy = create_activity_quality_policy('weight_aktuell')
|
|
assert policy is None
|
|
|
|
policy = create_activity_quality_policy('protein_avg')
|
|
assert policy is None
|
|
|
|
|
|
# ── Completeness Score Tests ──────────────────────────────────────────────────
|
|
|
|
def test_completeness_score_high():
|
|
"""High completeness score."""
|
|
metadata_dict = {
|
|
'category': 'Körper',
|
|
'description': 'Aktuelles Gewicht in kg',
|
|
'semantic_contract': 'Letzter verfügbarer Gewichtseintrag aus weight_log',
|
|
'source': {
|
|
'resolver': 'get_latest_weight',
|
|
'data_layer_module': 'body_metrics',
|
|
'source_tables': ['weight_log']
|
|
},
|
|
'type': 'atomic',
|
|
'time_window': 'latest',
|
|
'output_type': 'number',
|
|
'format_hint': '85.8 kg',
|
|
'quality_filter_policy': None,
|
|
'confidence_logic': {'supported': True}
|
|
}
|
|
|
|
score = calculate_completeness_score(metadata_dict)
|
|
assert score >= 80
|
|
|
|
|
|
def test_completeness_score_low():
|
|
"""Low completeness score."""
|
|
metadata_dict = {
|
|
'category': 'Unknown',
|
|
'description': '',
|
|
'semantic_contract': '',
|
|
'source': {'resolver': 'unknown'},
|
|
'type': 'legacy_unknown',
|
|
'time_window': 'unknown',
|
|
'output_type': 'unknown',
|
|
'format_hint': None
|
|
}
|
|
|
|
score = calculate_completeness_score(metadata_dict)
|
|
assert score < 50
|
|
|
|
|
|
# ── Integration Tests ─────────────────────────────────────────────────────────
|
|
|
|
def test_no_interpreted_without_provenance():
|
|
"""Interpreted type only for proven AI/prompt sources."""
|
|
# This would need to check actual metadata
|
|
# Placeholder for integration test
|
|
pass
|
|
|
|
|
|
def test_legacy_compatibility_maintained():
|
|
"""Legacy export format still works."""
|
|
# This would test that existing consumers still work
|
|
pass
|
|
|
|
|
|
# ── Run Tests ─────────────────────────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|