feat(tags): Title-Case normalization on create/rename — #701 (core)
Tags now normalize to Title Case + collapsed whitespace at the central
TagService.find_or_create (and rename) — so manual entry, the create API, and
anything routed through find_or_create produce the canonical form. The lookup is
case-insensitive, so a differently-cased entry finds the existing tag instead of
forking a case-variant duplicate ('hatsune miku' / 'HATSUNE MIKU' → one tag).
normalize_tag_name uses per-word capitalize (not str.title(), which mangles
apostrophes) and folds ALL-CAPS input. Existing tags keep their current casing
until touched — a retro-normalize maintenance pass (Title-Case + merge
case-collisions) is the follow-up to convert the back-catalog.
Test: create title-cases + collapses whitespace; case/whitespace variants dedupe
to one tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,19 @@ async def test_character_with_fandom(db):
|
||||
assert char.fandom_id == fandom.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_or_create_titlecases_and_dedups_case_insensitive(db):
|
||||
"""#701: tags normalize to Title Case + collapsed whitespace on create, and a
|
||||
differently-cased entry finds the existing tag instead of forking."""
|
||||
svc = TagService(db)
|
||||
t1 = await svc.find_or_create("hatsune miku", TagKind.character)
|
||||
assert t1.name == "Hatsune Miku"
|
||||
t2 = await svc.find_or_create("HATSUNE miku", TagKind.character)
|
||||
assert t2.id == t1.id # case + whitespace variant → same tag, no fork
|
||||
g = await svc.find_or_create(" looking at viewer ", TagKind.general)
|
||||
assert g.name == "Looking At Viewer"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_character_same_name_different_fandoms(db):
|
||||
svc = TagService(db)
|
||||
|
||||
Reference in New Issue
Block a user