- Introduced new routes and API endpoints for managing personal reference values. - Updated the SettingsPage to include a section for reference values with navigation to manage them. - Enhanced the backend to support reference values in the data layer and versioning. - Added necessary imports and UI components for a seamless user experience.
139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
"""
|
|
Unit tests: Layer 1 training profile resolver scaffold.
|
|
|
|
No database; pure template + algorithm + resolver behavior.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from data_layer.training_profile import (
|
|
CalculationTemplate,
|
|
DimensionSpec,
|
|
FocusAreaMapping,
|
|
TrainingEvaluationResult,
|
|
resolve_for_base_profile,
|
|
resolve_training_evaluation,
|
|
)
|
|
from data_layer.training_profile.algorithms.registry import (
|
|
get_algorithm,
|
|
list_algorithm_ids,
|
|
register_algorithm,
|
|
)
|
|
from data_layer.training_profile.models import AlgorithmRunResult
|
|
from data_layer.training_profile.profiles.registry import get_training_base_profile
|
|
from data_layer.training_profile.templates.registry import get_calculation_template
|
|
|
|
|
|
class TestAlgorithmRegistry:
|
|
def test_builtin_algorithms_registered(self):
|
|
ids = list_algorithm_ids()
|
|
assert "threshold_band" in ids
|
|
assert "linear_range" in ids
|
|
|
|
def test_get_algorithm_runs_threshold(self):
|
|
fn = get_algorithm("threshold_band")
|
|
r = fn(
|
|
inputs={"avg_hr": 130.0},
|
|
params={
|
|
"value_key": "avg_hr",
|
|
"bands": [
|
|
{"max": 120, "score": 0.2},
|
|
{"max": 150, "score": 0.8},
|
|
{"max": None, "score": 1.0},
|
|
],
|
|
},
|
|
)
|
|
assert r.normalized_score == 0.8
|
|
|
|
def test_duplicate_register_raises(self):
|
|
def dummy(*, inputs, params):
|
|
return AlgorithmRunResult(0.0, 0.0, [])
|
|
|
|
with pytest.raises(ValueError, match="already registered"):
|
|
register_algorithm("threshold_band", dummy)
|
|
|
|
|
|
class TestResolver:
|
|
def test_example_template_resolves(self):
|
|
tpl = get_calculation_template("scaffold_example_aerobic_v1")
|
|
result = resolve_training_evaluation(
|
|
activity_inputs={
|
|
"avg_hr": 135.0,
|
|
"duration_min": 45.0,
|
|
"distance_km": 10.0,
|
|
},
|
|
template=tpl,
|
|
)
|
|
assert isinstance(result, TrainingEvaluationResult)
|
|
assert result.template_id == "scaffold_example_aerobic_v1"
|
|
assert result.confidence == "high"
|
|
assert "aerobic_endurance" in result.focus_area_contributions
|
|
assert len(result.dimension_results) == 2
|
|
for dr in result.dimension_results:
|
|
assert dr.missing_inputs == []
|
|
|
|
def test_missing_required_input_skips_dimension(self):
|
|
tpl = get_calculation_template("scaffold_example_aerobic_v1")
|
|
result = resolve_training_evaluation(
|
|
activity_inputs={"avg_hr": 135.0},
|
|
template=tpl,
|
|
)
|
|
assert result.confidence in ("medium", "low", "insufficient")
|
|
skipped = [d for d in result.dimension_results if d.evidence.get("skipped")]
|
|
assert len(skipped) >= 1
|
|
|
|
def test_base_profile_filters_dimensions(self):
|
|
profile = get_training_base_profile("scaffold_strength_base")
|
|
tpl = get_calculation_template(profile.default_template_id)
|
|
result = resolve_training_evaluation(
|
|
activity_inputs={"duration_min": 50.0},
|
|
template=tpl,
|
|
base_profile=profile,
|
|
)
|
|
assert len(result.dimension_results) == 1
|
|
assert result.dimension_results[0].dimension_key == "effort"
|
|
|
|
def test_resolve_for_base_profile_convenience(self):
|
|
result = resolve_for_base_profile(
|
|
activity_inputs={"duration_min": 40.0},
|
|
base_profile_key="scaffold_strength_base",
|
|
include_trace=True,
|
|
)
|
|
assert result.base_profile_key == "scaffold_strength_base"
|
|
assert result.trace is not None
|
|
assert "effort" in result.trace
|
|
|
|
def test_to_serializable(self):
|
|
tpl = get_calculation_template("scaffold_example_strength_v1")
|
|
r = resolve_training_evaluation(
|
|
activity_inputs={"duration_min": 45.0},
|
|
template=tpl,
|
|
)
|
|
d = r.to_serializable()
|
|
assert d["template_id"] == tpl.id
|
|
assert "focus_area_contributions" in d
|
|
assert isinstance(d["dimension_results"], list)
|
|
|
|
|
|
class TestCustomTemplate:
|
|
def test_unknown_algorithm_raises(self):
|
|
bad = CalculationTemplate(
|
|
id="bad",
|
|
version="1",
|
|
label="bad",
|
|
dimensions=(
|
|
DimensionSpec(
|
|
key="x",
|
|
algorithm_id="does_not_exist",
|
|
inputs=("a",),
|
|
params={},
|
|
maps_to=(FocusAreaMapping("strength", 1.0),),
|
|
),
|
|
),
|
|
)
|
|
with pytest.raises(KeyError):
|
|
resolve_training_evaluation(
|
|
activity_inputs={"a": 1.0},
|
|
template=bad,
|
|
)
|