fix(tags): Title-Case operator-entered tags at create endpoint only (plan #701)
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 24s
CI / integration (push) Successful in 3m2s
CI / lint (push) Successful in 2s

normalize_tag_name (per-word capitalize + whitespace collapse) is applied in
the POST /api/tags handler so operator-entered tags get clean display casing.
It is NOT applied in the shared find_or_create / rename paths — those are used
by the ML tagger and allowlist matching, which must preserve the booru
vocabulary's original casing (Title-Casing it broke apply_allowlist matching).

find_or_create / rename keep case-insensitive lookup + clash detection so a
differently-cased entry dedups onto the existing tag instead of forking.

Tests updated to expect Title-Cased create output (sunset→Sunset,
character:Saber→Character:saber, http://example.com→Http://example.com) and a
dedicated normalize_tag_name unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:57:29 -04:00
parent 62cca64dce
commit 23f452021f
4 changed files with 37 additions and 17 deletions
+6
View File
@@ -14,6 +14,7 @@ from ..services.tag_service import (
TagMergeConflict,
TagService,
TagValidationError,
normalize_tag_name,
)
from ..utils.tag_prefix import parse_kind_prefix
@@ -141,6 +142,11 @@ async def create_tag():
fandom_id = body.get("fandom_id")
# #701: Title-Case operator-entered tags. Only here (the explicit create
# endpoint), NOT in the shared find_or_create — the ML tagger uses that path
# and must keep the booru vocabulary's casing for allowlist matching.
name = normalize_tag_name(name)
async with get_session() as session:
svc = TagService(session)
try: