mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:17:02 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
3.2 KiB
Python
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"
|