signal-bot/tests/test_folding.py
Krzysztof kuhy Rudnicki b70730fc7a fix: commands resolve when typed on another alphabet's keyboard
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 14:22:43 +02:00

102 lines
3.2 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Folding a typed command down to a comparable skeleton.
Every non-ASCII literal here is an escape. These tests exist because two
characters looked identical, so a reader must be able to see which codepoint
each case actually means.
"""
from __future__ import annotations
import pytest
from signal_bot.folding import TABLE, fold
# The regression itself: `!gecko` as it arrives from a Russian keyboard, with
# U+0435 for the `e` and U+043E for the `o`.
RUSSIAN_GECKO = "!g\u0435ck\u043e"
def test_the_cyrillic_gecko_that_started_this() -> None:
assert fold(RUSSIAN_GECKO) == "!gecko"
assert RUSSIAN_GECKO != "!gecko"
@pytest.mark.parametrize(
("typed", "expected"),
[
# Cyrillic
("!\u043a\u043e\u0442", "!kot"),
("!\u0441\u0430\u0442", "!cat"),
("!d\u043eg", "!dog"),
# Greek
("!d\u03bfg", "!dog"),
("!\u03bau\u03bf\u03c4", "!kuot"),
# Latin small capitals
("!\u1d0b\u1d0f\u1d1b", "!kot"),
# Latin letters that are not ASCII
("!d\u00f8g", "!dog"),
("!p\u0131es", "!pies"),
# Leetspeak
("!g3ck0", "!gecko"),
("!d0g", "!dog"),
("!c4t", "!cat"),
("!p13s", "!pies"),
("!5zczek", "!szczek"),
("!ca7", "!cat"),
# Compatibility forms NFKC already handles
("!\uff44\uff4f\uff47", "!dog"),
("!\U0001d5f8\U0001d5fc\U0001d601", "!kot"),
# Diacritics decompose away
("!papie\u017c", "!papiez"),
("!k\u00f3t", "!kot"),
# Invisible characters are dropped
("!d\u200bog", "!dog"),
("!d\u200dog", "!dog"),
("!dog\u00ad", "!dog"),
("!\ufeffdog", "!dog"),
# Case and surrounding whitespace
(" !DOG ", "!dog"),
("!Dog", "!dog"),
(" !dog ", "!dog"),
],
)
def test_fold_maps_lookalikes_onto_ascii(typed: str, expected: str) -> None:
assert fold(typed) == expected
def test_fold_keeps_internal_whitespace() -> None:
"""Argument support (0.0.6) splits on it, so folding must not eat it."""
assert fold("!suggest !gecko co ma robic") == "!suggest !gecko co ma robic"
def test_fold_leaves_ordinary_text_alone() -> None:
assert fold("!dog") == "!dog"
assert fold("hello there") == "hello there"
assert fold("") == ""
def test_fold_is_idempotent() -> None:
"""Folding twice must equal folding once, or triggers drift from input."""
for typed in (RUSSIAN_GECKO, "!g3ck0", "!\u1d0b\u1d0f\u1d1b", "!papie\u017c"):
assert fold(fold(typed)) == fold(typed)
def test_fold_does_not_collapse_genuinely_different_commands() -> None:
"""The table must not be so eager that distinct commands become one."""
assert fold("!cat") != fold("!dog")
assert fold("!kot") != fold("!kots")
assert fold("!gekon") != fold("!geko")
def test_every_mapping_target_is_ascii() -> None:
"""A mapping onto another non-ASCII character would need folding again."""
for replacement in TABLE.values():
assert replacement.isascii()
def test_table_covers_both_alphabets_that_bit_us() -> None:
assert TABLE[0x043E] == "o"
assert TABLE[0x0435] == "e"
assert TABLE[0x03BF] == "o"