New SQLite table `exercise_aliases (alias, canonical, source)` seeded with ~40 common gym shorthand entries (OHP, RDL, "Bench", "Squat", plural/singular drifts, slang). Lookups go through this table first, then fall through to the strict exercise_db matcher — so the strict matcher's "false negative for ambiguous single tokens" property is preserved while still resolving every-day vocabulary. Schema decision: every seed row is tagged `source='seed'` and re-seeded on every init_db (deleted-then-reinserted), so editing the seed dict in code is the one source of truth. User-inserted rows are tagged `source='user'` and never touched by re-seeding. Migration path covers existing DBs where the `source` column didn't exist (those rows tagged 'seed' on first migration, then refreshed from the current seed). New helper db.lookup_exercise(name) wraps the alias resolution + the exercise_db.lookup() call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.5 KiB
Python
66 lines
2.5 KiB
Python
"""Tests for the static exercise reference matcher."""
|
|
import exercise_db
|
|
|
|
|
|
class TestLookup:
|
|
def test_loads_bundled_data(self):
|
|
# Free-Exercise-DB ships ~870 entries; just sanity-check it's non-empty.
|
|
assert len(exercise_db.ALL) > 500
|
|
|
|
def test_exact_match(self):
|
|
m = exercise_db.lookup("Pullups")
|
|
assert m is not None
|
|
assert m["name"] == "Pullups"
|
|
assert "lats" in m["primary_muscles"]
|
|
|
|
def test_case_insensitive(self):
|
|
a = exercise_db.lookup("PULLUPS")
|
|
b = exercise_db.lookup("pullups")
|
|
assert a == b
|
|
assert a["name"] == "Pullups"
|
|
|
|
def test_hyphen_matches_compressed(self):
|
|
# User types "Pull-ups", DB has "Pullups". Compressed form catches it.
|
|
m = exercise_db.lookup("Pull-ups")
|
|
assert m is not None
|
|
assert m["name"] == "Pullups"
|
|
|
|
def test_multi_word_substring(self):
|
|
m = exercise_db.lookup("Romanian deadlift")
|
|
assert m is not None
|
|
assert m["name"] == "Romanian Deadlift"
|
|
assert "hamstrings" in m["primary_muscles"]
|
|
|
|
def test_ambiguous_single_token_returns_none(self):
|
|
# Lots of DB entries contain "Bench" / "Squat" / "Deadlift" as one
|
|
# token. Returning the shortest would mislead ("Bench" → "Bench Dips"
|
|
# → triceps). Refuse instead.
|
|
assert exercise_db.lookup("Bench") is None
|
|
assert exercise_db.lookup("Squat") is None
|
|
assert exercise_db.lookup("Deadlift") is None
|
|
|
|
def test_nonsense_returns_none(self):
|
|
assert exercise_db.lookup("flarbenstompf") is None
|
|
assert exercise_db.lookup("") is None
|
|
assert exercise_db.lookup(" ") is None
|
|
|
|
def test_short_acronyms_dont_substring_match_characters(self):
|
|
# Regression: "RDL" used to match "Hurdle Hops" because "rdl"
|
|
# appears as a character substring inside "hu**rdl**ehops".
|
|
assert exercise_db.lookup("RDL") is None
|
|
assert exercise_db.lookup("OHP") is None
|
|
|
|
def test_multi_token_requires_full_coverage(self):
|
|
# Regression: "BB Row" used to match "Sled Row" because both share
|
|
# the "row" token. Strict 100% coverage prevents this.
|
|
assert exercise_db.lookup("BB Row") is None
|
|
|
|
def test_returned_shape(self):
|
|
m = exercise_db.lookup("Pullups")
|
|
# The slim view drops `instructions` and `images`.
|
|
assert set(m.keys()) >= {
|
|
"name", "primary_muscles", "secondary_muscles",
|
|
"equipment", "category", "level",
|
|
}
|
|
assert "instructions" not in m
|
|
assert "images" not in m
|