Unraid deployment fixes and generator improvements
- Add Next.js rewrites to proxy API calls through same origin (fixes login/media on werkout.treytartt.com) - Fix mediaUrl() in DayCard and ExerciseRow to use relative paths in production - Add proxyTimeout for long-running workout generation endpoints - Add CSRF trusted origin for treytartt.com - Split docker-compose into production (Unraid) and dev configs - Show display_name and descriptions on workout type cards - Generator: rules engine improvements, movement enforcement, exercise selector updates - Add new test files for rules drift, workout research generation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ Tests for _build_working_supersets() — Items #4, #6, #7:
|
||||
- Modality consistency check (duration_bias warning)
|
||||
- Straight-set strength (first superset = single main lift)
|
||||
"""
|
||||
from datetime import date
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch, MagicMock, PropertyMock
|
||||
@@ -16,10 +17,12 @@ from generator.models import (
|
||||
WorkoutType,
|
||||
)
|
||||
from generator.services.workout_generator import (
|
||||
FINAL_CONFORMANCE_MAX_RETRIES,
|
||||
WorkoutGenerator,
|
||||
STRENGTH_WORKOUT_TYPES,
|
||||
WORKOUT_TYPE_DEFAULTS,
|
||||
)
|
||||
from generator.rules_engine import RuleViolation, validate_workout
|
||||
from registered_user.models import RegisteredUser
|
||||
|
||||
User = get_user_model()
|
||||
@@ -58,6 +61,18 @@ class MovementEnforcementTestBase(TestCase):
|
||||
superset_size_min=3,
|
||||
superset_size_max=6,
|
||||
)
|
||||
cls.core_type = WorkoutType.objects.filter(name='core_training').first()
|
||||
if cls.core_type is None:
|
||||
cls.core_type = WorkoutType.objects.create(
|
||||
name='core_training',
|
||||
typical_rest_between_sets=30,
|
||||
typical_intensity='medium',
|
||||
rep_range_min=10,
|
||||
rep_range_max=20,
|
||||
duration_bias=0.5,
|
||||
superset_size_min=3,
|
||||
superset_size_max=5,
|
||||
)
|
||||
|
||||
# Create MovementPatternOrder records
|
||||
MovementPatternOrder.objects.create(
|
||||
@@ -169,6 +184,58 @@ class TestMovementPatternEnforcement(MovementEnforcementTestBase):
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_retries_when_superset_has_duplicate_focus(self):
|
||||
"""Generator should retry when a working superset repeats focus family."""
|
||||
pref = self._make_preference()
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
curl_a = self._create_mock_exercise(
|
||||
'Alternating Bicep Curls',
|
||||
movement_patterns='upper pull',
|
||||
is_compound=False,
|
||||
exercise_tier='accessory',
|
||||
)
|
||||
curl_b = self._create_mock_exercise(
|
||||
'Bicep Curls',
|
||||
movement_patterns='upper pull',
|
||||
is_compound=False,
|
||||
exercise_tier='accessory',
|
||||
)
|
||||
pull = self._create_mock_exercise('Bent Over Row', movement_patterns='upper pull')
|
||||
hinge = self._create_mock_exercise('Romanian Deadlift', movement_patterns='hip hinge')
|
||||
|
||||
gen.exercise_selector.select_exercises.side_effect = [
|
||||
[curl_a, curl_b], # rejected: duplicate focus
|
||||
[pull, hinge], # accepted
|
||||
]
|
||||
gen.exercise_selector.balance_stretch_positions.side_effect = lambda exs, **_: exs
|
||||
|
||||
muscle_split = {
|
||||
'muscles': ['upper back', 'biceps'],
|
||||
'split_type': 'pull',
|
||||
'label': 'Pull',
|
||||
}
|
||||
wt_params = dict(WORKOUT_TYPE_DEFAULTS['hiit'])
|
||||
wt_params['num_supersets'] = (1, 1)
|
||||
wt_params['exercises_per_superset'] = (2, 2)
|
||||
wt_params['duration_bias'] = 0.0
|
||||
|
||||
supersets = gen._build_working_supersets(muscle_split, self.hiit_type, wt_params)
|
||||
self.assertEqual(len(supersets), 1)
|
||||
self.assertGreaterEqual(gen.exercise_selector.select_exercises.call_count, 2)
|
||||
|
||||
names = [
|
||||
entry['exercise'].name
|
||||
for entry in supersets[0].get('exercises', [])
|
||||
]
|
||||
self.assertNotEqual(
|
||||
set(names),
|
||||
{'Alternating Bicep Curls', 'Bicep Curls'},
|
||||
f'Expected duplicate-focus superset to be retried, got {names}',
|
||||
)
|
||||
|
||||
pref.delete()
|
||||
|
||||
|
||||
class TestStrengthStraightSets(MovementEnforcementTestBase):
|
||||
"""Item #7: First working superset in strength = single main lift."""
|
||||
@@ -288,13 +355,19 @@ class TestStrengthStraightSets(MovementEnforcementTestBase):
|
||||
|
||||
# Should have multiple supersets
|
||||
if len(supersets) >= 2:
|
||||
# Check that the second superset's select_exercises call
|
||||
# requested count >= 2 (min_ex_per_ss)
|
||||
second_call = gen.exercise_selector.select_exercises.call_args_list[1]
|
||||
count_arg = second_call.kwargs.get('count')
|
||||
if count_arg is None and len(second_call.args) > 1:
|
||||
count_arg = second_call.args[1]
|
||||
self.assertGreaterEqual(count_arg, 2)
|
||||
# Retries may add extra calls; assert at least one non-first
|
||||
# working-superset request asks for 2+ exercises.
|
||||
observed_counts = []
|
||||
for call in gen.exercise_selector.select_exercises.call_args_list:
|
||||
count_arg = call.kwargs.get('count')
|
||||
if count_arg is None and len(call.args) > 1:
|
||||
count_arg = call.args[1]
|
||||
if count_arg is not None:
|
||||
observed_counts.append(count_arg)
|
||||
self.assertTrue(
|
||||
any(c >= 2 for c in observed_counts),
|
||||
f"Expected at least one accessory superset request >=2 exercises, got {observed_counts}",
|
||||
)
|
||||
|
||||
pref.delete()
|
||||
|
||||
@@ -330,6 +403,68 @@ class TestStrengthStraightSets(MovementEnforcementTestBase):
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_strength_first_superset_survives_post_processing(self):
|
||||
"""generate_single_workout should preserve first strength straight set."""
|
||||
pref = self._make_preference(primary_goal='strength')
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
main_lift = self._create_mock_exercise('Back Squat', exercise_tier='primary')
|
||||
accessory_1 = self._create_mock_exercise('DB Row', exercise_tier='secondary')
|
||||
accessory_2 = self._create_mock_exercise('RDL', exercise_tier='secondary')
|
||||
accessory_3 = self._create_mock_exercise('Lat Pulldown', exercise_tier='accessory')
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._get_final_conformance_violations = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
gen._build_working_supersets = MagicMock(return_value=[
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 5,
|
||||
'rest_between_rounds': 120,
|
||||
'modality': 'reps',
|
||||
'exercises': [
|
||||
{'exercise': main_lift, 'reps': 5, 'order': 1},
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'Working Set 2',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 90,
|
||||
'modality': 'reps',
|
||||
'exercises': [
|
||||
{'exercise': accessory_1, 'reps': 10, 'order': 1},
|
||||
{'exercise': accessory_2, 'reps': 10, 'order': 2},
|
||||
{'exercise': accessory_3, 'reps': 12, 'order': 3},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
muscle_split = {
|
||||
'muscles': ['quads', 'hamstrings'],
|
||||
'split_type': 'lower',
|
||||
'label': 'Lower',
|
||||
}
|
||||
workout_spec = gen.generate_single_workout(
|
||||
muscle_split=muscle_split,
|
||||
workout_type=self.strength_type,
|
||||
scheduled_date=date(2026, 3, 2),
|
||||
)
|
||||
|
||||
working = [
|
||||
ss for ss in workout_spec.get('supersets', [])
|
||||
if ss.get('name', '').startswith('Working')
|
||||
]
|
||||
self.assertGreaterEqual(len(working), 1)
|
||||
self.assertEqual(
|
||||
len(working[0].get('exercises', [])),
|
||||
1,
|
||||
f'Expected first strength working set to stay at 1 exercise, got: {working[0]}',
|
||||
)
|
||||
|
||||
pref.delete()
|
||||
|
||||
|
||||
class TestModalityConsistency(MovementEnforcementTestBase):
|
||||
"""Item #6: Modality consistency warning for duration-dominant workouts."""
|
||||
@@ -503,3 +638,357 @@ class TestModalityConsistency(MovementEnforcementTestBase):
|
||||
)
|
||||
|
||||
pref.delete()
|
||||
|
||||
|
||||
class TestFinalConformance(MovementEnforcementTestBase):
|
||||
"""Strict final conformance enforcement for assembled workouts."""
|
||||
|
||||
def test_core_workout_respects_type_max_exercise_cap(self):
|
||||
"""Core workouts should be trimmed to the calibrated max (8 working exercises)."""
|
||||
pref = self._make_preference(primary_goal='general_fitness')
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._get_final_conformance_violations = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
|
||||
working_exercises = [
|
||||
{'exercise': self._create_mock_exercise(f'Core Push {i}', movement_patterns='upper push, core'), 'reps': 12, 'order': i + 1}
|
||||
for i in range(6)
|
||||
]
|
||||
more_working_exercises = [
|
||||
{'exercise': self._create_mock_exercise(f'Core Pull {i}', movement_patterns='upper pull, core'), 'reps': 12, 'order': i + 1}
|
||||
for i in range(6)
|
||||
]
|
||||
|
||||
gen._build_working_supersets = MagicMock(return_value=[
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 30,
|
||||
'modality': 'reps',
|
||||
'exercises': working_exercises,
|
||||
},
|
||||
{
|
||||
'name': 'Working Set 2',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 30,
|
||||
'modality': 'reps',
|
||||
'exercises': more_working_exercises,
|
||||
},
|
||||
])
|
||||
|
||||
workout_spec = gen.generate_single_workout(
|
||||
muscle_split={
|
||||
'muscles': ['core', 'abs', 'obliques'],
|
||||
'split_type': 'core',
|
||||
'label': 'Core Day',
|
||||
},
|
||||
workout_type=self.core_type,
|
||||
scheduled_date=date(2026, 3, 2),
|
||||
)
|
||||
|
||||
working = [
|
||||
ss for ss in workout_spec.get('supersets', [])
|
||||
if ss.get('name', '').startswith('Working')
|
||||
]
|
||||
total_working = sum(len(ss.get('exercises', [])) for ss in working)
|
||||
self.assertLessEqual(
|
||||
total_working, 8,
|
||||
f'Expected core workout to cap at 8 working exercises, got {total_working}',
|
||||
)
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_core_cap_removes_extra_minimum_supersets(self):
|
||||
"""When all sets are already at minimum size, remove trailing sets to hit cap."""
|
||||
pref = self._make_preference(primary_goal='general_fitness')
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._get_final_conformance_violations = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
|
||||
working_supersets = []
|
||||
for idx in range(6):
|
||||
push = self._create_mock_exercise(
|
||||
f'Push {idx}',
|
||||
movement_patterns='upper push',
|
||||
)
|
||||
pull = self._create_mock_exercise(
|
||||
f'Pull {idx}',
|
||||
movement_patterns='upper pull',
|
||||
)
|
||||
working_supersets.append({
|
||||
'name': f'Working Set {idx + 1}',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 30,
|
||||
'modality': 'reps',
|
||||
'exercises': [
|
||||
{'exercise': push, 'reps': 12, 'order': 1},
|
||||
{'exercise': pull, 'reps': 12, 'order': 2},
|
||||
],
|
||||
})
|
||||
|
||||
gen._build_working_supersets = MagicMock(return_value=working_supersets)
|
||||
|
||||
workout_spec = gen.generate_single_workout(
|
||||
muscle_split={
|
||||
'muscles': ['core', 'abs', 'obliques'],
|
||||
'split_type': 'core',
|
||||
'label': 'Core Day',
|
||||
},
|
||||
workout_type=self.core_type,
|
||||
scheduled_date=date(2026, 3, 2),
|
||||
)
|
||||
|
||||
working = [
|
||||
ss for ss in workout_spec.get('supersets', [])
|
||||
if ss.get('name', '').startswith('Working')
|
||||
]
|
||||
total_working = sum(len(ss.get('exercises', [])) for ss in working)
|
||||
self.assertLessEqual(total_working, 8)
|
||||
self.assertLessEqual(len(working), 4)
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_pad_to_fill_respects_type_cap(self):
|
||||
"""Padding should stop when workout-type max working-exercise cap is reached."""
|
||||
pref = self._make_preference(primary_goal='general_fitness')
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._estimate_total_time = MagicMock(return_value=0)
|
||||
gen.exercise_selector.select_exercises.return_value = [
|
||||
self._create_mock_exercise('Pad Exercise', movement_patterns='upper pull')
|
||||
]
|
||||
|
||||
base_ex_a = self._create_mock_exercise('Base A', movement_patterns='upper push')
|
||||
base_ex_b = self._create_mock_exercise('Base B', movement_patterns='upper pull')
|
||||
workout_spec = {
|
||||
'supersets': [
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 30,
|
||||
'modality': 'reps',
|
||||
'exercises': [
|
||||
{'exercise': base_ex_a, 'reps': 12, 'order': 1},
|
||||
{'exercise': base_ex_b, 'reps': 12, 'order': 2},
|
||||
{'exercise': base_ex_a, 'reps': 12, 'order': 3},
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'Working Set 2',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 30,
|
||||
'modality': 'reps',
|
||||
'exercises': [
|
||||
{'exercise': base_ex_b, 'reps': 12, 'order': 1},
|
||||
{'exercise': base_ex_a, 'reps': 12, 'order': 2},
|
||||
{'exercise': base_ex_b, 'reps': 12, 'order': 3},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
wt_params = dict(WORKOUT_TYPE_DEFAULTS['core'])
|
||||
wt_params['duration_bias'] = 0.0
|
||||
|
||||
padded = gen._pad_to_fill(
|
||||
workout_spec=workout_spec,
|
||||
max_duration_sec=3600,
|
||||
muscle_split={
|
||||
'muscles': ['core', 'abs'],
|
||||
'split_type': 'core',
|
||||
'label': 'Core Day',
|
||||
},
|
||||
wt_params=wt_params,
|
||||
workout_type=self.core_type,
|
||||
)
|
||||
|
||||
total_working = sum(
|
||||
len(ss.get('exercises', []))
|
||||
for ss in padded.get('supersets', [])
|
||||
if ss.get('name', '').startswith('Working')
|
||||
)
|
||||
self.assertLessEqual(total_working, 8)
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_compound_ordering_uses_validator_definition(self):
|
||||
"""Accessory-tagged entries should not be treated as compounds in ordering."""
|
||||
pref = self._make_preference(primary_goal='general_fitness')
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
accessory_flagged_compound = self._create_mock_exercise(
|
||||
'Accessory Marked Compound',
|
||||
is_compound=True,
|
||||
exercise_tier='accessory',
|
||||
movement_patterns='upper push',
|
||||
)
|
||||
true_compound = self._create_mock_exercise(
|
||||
'Primary Compound',
|
||||
is_compound=True,
|
||||
exercise_tier='secondary',
|
||||
movement_patterns='upper pull',
|
||||
)
|
||||
workout_spec = {
|
||||
'supersets': [
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 45,
|
||||
'modality': 'reps',
|
||||
'exercises': [
|
||||
{'exercise': accessory_flagged_compound, 'reps': 10, 'order': 1},
|
||||
{'exercise': true_compound, 'reps': 8, 'order': 2},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
gen._enforce_compound_first_order(workout_spec, is_strength_workout=False)
|
||||
violations = validate_workout(workout_spec, 'hiit', 'general_fitness')
|
||||
compound_order_violations = [
|
||||
v for v in violations
|
||||
if v.rule_id == 'compound_before_isolation'
|
||||
]
|
||||
self.assertEqual(len(compound_order_violations), 0)
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_final_warning_triggers_regeneration(self):
|
||||
"""A final warning should trigger full regeneration before returning."""
|
||||
pref = self._make_preference()
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
|
||||
ex = self._create_mock_exercise('Balanced Pull', movement_patterns='upper pull')
|
||||
gen._build_working_supersets = MagicMock(return_value=[
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 45,
|
||||
'modality': 'reps',
|
||||
'exercises': [{'exercise': ex, 'reps': 10, 'order': 1}],
|
||||
},
|
||||
])
|
||||
|
||||
gen._get_final_conformance_violations = MagicMock(side_effect=[
|
||||
[RuleViolation(
|
||||
rule_id='exercise_count_cap',
|
||||
severity='warning',
|
||||
message='Too many exercises',
|
||||
)],
|
||||
[],
|
||||
])
|
||||
|
||||
gen.generate_single_workout(
|
||||
muscle_split={
|
||||
'muscles': ['upper back', 'lats'],
|
||||
'split_type': 'pull',
|
||||
'label': 'Pull Day',
|
||||
},
|
||||
workout_type=self.hiit_type,
|
||||
scheduled_date=date(2026, 3, 3),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
gen._build_working_supersets.call_count, 2,
|
||||
'Expected regeneration after final warning.',
|
||||
)
|
||||
pref.delete()
|
||||
|
||||
def test_unresolved_final_violations_raise_error(self):
|
||||
"""Generator should fail fast when conformance cannot be achieved."""
|
||||
pref = self._make_preference()
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
|
||||
ex = self._create_mock_exercise('Push Only', movement_patterns='upper push')
|
||||
gen._build_working_supersets = MagicMock(return_value=[
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 45,
|
||||
'modality': 'reps',
|
||||
'exercises': [{'exercise': ex, 'reps': 10, 'order': 1}],
|
||||
},
|
||||
])
|
||||
gen._get_final_conformance_violations = MagicMock(return_value=[
|
||||
RuleViolation(
|
||||
rule_id='push_pull_ratio',
|
||||
severity='warning',
|
||||
message='Pull:push ratio too low',
|
||||
),
|
||||
])
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
gen.generate_single_workout(
|
||||
muscle_split={
|
||||
'muscles': ['chest', 'triceps'],
|
||||
'split_type': 'push',
|
||||
'label': 'Push Day',
|
||||
},
|
||||
workout_type=self.hiit_type,
|
||||
scheduled_date=date(2026, 3, 4),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
gen._build_working_supersets.call_count,
|
||||
FINAL_CONFORMANCE_MAX_RETRIES + 1,
|
||||
)
|
||||
pref.delete()
|
||||
|
||||
def test_info_violation_is_not_blocking(self):
|
||||
"""Info-level rules should not fail generation in strict mode."""
|
||||
pref = self._make_preference()
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
|
||||
ex = self._create_mock_exercise('Compound Lift', movement_patterns='upper pull')
|
||||
gen._build_working_supersets = MagicMock(return_value=[
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 45,
|
||||
'modality': 'reps',
|
||||
'exercises': [{'exercise': ex, 'reps': 8, 'order': 1}],
|
||||
},
|
||||
])
|
||||
gen._get_final_conformance_violations = MagicMock(return_value=[
|
||||
RuleViolation(
|
||||
rule_id='compound_before_isolation',
|
||||
severity='info',
|
||||
message='Compound exercises should generally appear before isolation.',
|
||||
),
|
||||
])
|
||||
|
||||
workout = gen.generate_single_workout(
|
||||
muscle_split={
|
||||
'muscles': ['upper back'],
|
||||
'split_type': 'pull',
|
||||
'label': 'Pull Day',
|
||||
},
|
||||
workout_type=self.strength_type,
|
||||
scheduled_date=date(2026, 3, 5),
|
||||
)
|
||||
|
||||
self.assertIsInstance(workout, dict)
|
||||
self.assertEqual(gen._build_working_supersets.call_count, 1)
|
||||
pref.delete()
|
||||
|
||||
Reference in New Issue
Block a user