Skip to content

Commit f7a6622

Browse files
test(telegram): de-phantom UTF-16 split test, add slash-routing coverage
Make test_trailing_text_split_uses_utf16_offsets genuinely exercise the UTF-16 code-unit split. The prior input ("/p😀g hello") converged: a separating space meant naive code-point slicing and UTF-16-LE offset slicing both yielded "hello" after lstrip, so the test passed against a naive text[entity_length:] implementation. The new input ("/p😀ghello") abuts the trailing text with no separator, so the naive slice over- advances and drops the leading "h" ("ello"), while the UTF-16-aware split keeps it ("hello") — verified to FAIL against a reverted naive slice. Add targeted regression coverage, each pinned to a distinct mutation: - @bot targeting is case-insensitive (/ping@MyBot routes when user_name is "mybot"); fails under a case-sensitive comparison. - slash gating is scoped to update.message only: edited_message and channel_post carrying a leading bot_command entity route to process_message, never the slash handler; fails if the gate widens to message_update. - empty-command guard: /@Mybot and a bare / yield no slash command and route to process_message (matches upstream's if (!commandName)); fails if the guard is removed.
1 parent 9c426f6 commit f7a6622

1 file changed

Lines changed: 139 additions & 10 deletions

File tree

tests/test_telegram_webhook.py

Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -368,17 +368,28 @@ def test_trailing_text_split_uses_utf16_offsets(self):
368368
"""The command/trailing-text split is computed on UTF-16 code-unit
369369
offsets, not Python code points.
370370
371-
Telegram reports ``length`` in UTF-16 code units. The entity
371+
Telegram reports ``length`` in UTF-16 code units. The command token
372372
``/p😀g`` spans 4 code points but 5 UTF-16 units (the astral emoji is
373-
a surrogate pair), so the trailing-text split starts at unit 5. A
374-
naive code-point slice — ``text[5:]`` — over-advances and drops the
375-
leading ``h``; the UTF-16-aware slice keeps it.
373+
a surrogate pair). The trailing text abuts the token with **no
374+
separating space** (``/p😀ghello``), so the split offset cannot be
375+
masked by a ``lstrip`` after the fact:
376+
377+
* UTF-16-aware split at unit ``offset + length == 5`` lands exactly on
378+
the ``h`` and yields ``"hello"``.
379+
* A naive Python code-point slice ``text[5:]`` over-advances (the
380+
emoji counts as one code point, not two) and yields ``"ello"`` —
381+
the leading ``h`` is silently dropped.
382+
383+
Because there is no whitespace at the boundary, the two paths diverge
384+
in the final result, so this test FAILS against a naive
385+
``text[entity_length:]`` slice.
376386
"""
377387
adapter, _ = _slash_adapter_and_chat()
378-
# "/p😀g" = / p <emoji=2 units> g = 4 code points but 5 UTF-16 units.
388+
# "/p😀g" = / p <emoji=2 units> g = 4 code points but 5 UTF-16 units;
389+
# "hello" follows immediately with no separator.
379390
result = adapter.parse_slash_command(
380391
_sample_message(
381-
text="/p😀g hello",
392+
text="/p😀ghello",
382393
entities=[{"type": "bot_command", "offset": 0, "length": 5}],
383394
)
384395
)
@@ -388,12 +399,130 @@ def test_entity_text_split_naive_codepoint_would_diverge(self):
388399
"""Guards the UTF-16 split against a naive ``str`` slice regression.
389400
390401
``_slice_utf16`` and a naive code-point slice must diverge for astral
391-
text, proving the helper is load-bearing (not a no-op on ASCII)."""
402+
text, proving the helper is load-bearing (not a no-op on ASCII).
403+
With the trailing text abutting the token (no separator), the two
404+
slices return different strings that no ``lstrip`` can reconcile."""
392405
adapter, _ = _slash_adapter_and_chat()
393-
text = "/p😀g hello"
394-
assert adapter._slice_utf16(text, 5) == " hello"
406+
text = "/p😀ghello"
407+
# UTF-16-aware slice at code-unit 5 lands on the "h".
408+
assert adapter._slice_utf16(text, 5) == "hello"
395409
# The naive code-point slice over-advances and eats the leading "h".
396-
assert text[5:] == "hello"
410+
assert text[5:] == "ello"
411+
412+
@pytest.mark.asyncio
413+
async def test_at_bot_targeting_is_case_insensitive(self):
414+
"""``/ping@<MixedCase>`` still routes to the slash handler when the
415+
casing differs from ``user_name`` — the ``.lower()`` normalization on
416+
both sides is load-bearing (mutating it to a case-sensitive ``!=``
417+
drops this command to ``process_message``)."""
418+
adapter, chat = _slash_adapter_and_chat() # user_name == "mybot"
419+
body = json.dumps(
420+
{
421+
"update_id": 7,
422+
"message": _sample_message(
423+
text="/ping@MyBot hello",
424+
entities=[{"type": "bot_command", "offset": 0, "length": 11}],
425+
),
426+
}
427+
)
428+
429+
response = await adapter.handle_webhook(_make_request(body))
430+
assert response["status"] == 200
431+
432+
assert chat.process_slash_command.call_count == 1
433+
chat.process_message.assert_not_called()
434+
event = chat.process_slash_command.call_args.args[0]
435+
assert event.command == "/ping"
436+
assert event.text == "hello"
437+
438+
@pytest.mark.asyncio
439+
async def test_edited_message_with_bot_command_does_not_route_to_slash(self):
440+
"""Slash gating is scoped to ``update.message`` only: an
441+
``edited_message`` carrying a leading ``bot_command`` entity routes to
442+
the regular message path, never the slash handler (mutating the gate
443+
to read ``edited_message`` would mis-route the edit)."""
444+
adapter, chat = _slash_adapter_and_chat()
445+
body = json.dumps(
446+
{
447+
"update_id": 8,
448+
"edited_message": _sample_message(
449+
text="/ping@mybot hello",
450+
entities=[{"type": "bot_command", "offset": 0, "length": 11}],
451+
),
452+
}
453+
)
454+
455+
response = await adapter.handle_webhook(_make_request(body))
456+
assert response["status"] == 200
457+
458+
chat.process_slash_command.assert_not_called()
459+
assert chat.process_message.call_count == 1
460+
461+
@pytest.mark.asyncio
462+
async def test_channel_post_with_bot_command_does_not_route_to_slash(self):
463+
"""A ``channel_post`` carrying a leading ``bot_command`` entity routes
464+
to the regular message path, not the slash handler — slash gating only
465+
reads ``update.message`` (mirrors upstream ``update.message``)."""
466+
adapter, chat = _slash_adapter_and_chat()
467+
body = json.dumps(
468+
{
469+
"update_id": 9,
470+
"channel_post": _sample_message(
471+
text="/ping@mybot hello",
472+
entities=[{"type": "bot_command", "offset": 0, "length": 11}],
473+
),
474+
}
475+
)
476+
477+
response = await adapter.handle_webhook(_make_request(body))
478+
assert response["status"] == 200
479+
480+
chat.process_slash_command.assert_not_called()
481+
assert chat.process_message.call_count == 1
482+
483+
@pytest.mark.asyncio
484+
async def test_command_addressed_only_to_another_bot_routes_to_message(self):
485+
"""``/@bot`` (empty command name) yields no slash command — the
486+
``if not command_name: return None`` guard sends it to
487+
``process_message`` (matches upstream's ``if (!commandName)``)."""
488+
adapter, chat = _slash_adapter_and_chat()
489+
body = json.dumps(
490+
{
491+
"update_id": 10,
492+
"message": _sample_message(
493+
text="/@mybot hello",
494+
entities=[{"type": "bot_command", "offset": 0, "length": 7}],
495+
),
496+
}
497+
)
498+
499+
response = await adapter.handle_webhook(_make_request(body))
500+
assert response["status"] == 200
501+
502+
chat.process_slash_command.assert_not_called()
503+
assert chat.process_message.call_count == 1
504+
505+
@pytest.mark.asyncio
506+
async def test_bare_slash_routes_to_message(self):
507+
"""A bare ``/`` (no command name) yields no slash command and routes
508+
to ``process_message`` — the empty-``command_name`` guard, plus the
509+
``startswith('/')`` / ``offset == 0`` gating, all hold."""
510+
adapter, chat = _slash_adapter_and_chat()
511+
body = json.dumps(
512+
{
513+
"update_id": 11,
514+
"message": _sample_message(
515+
text="/ hello",
516+
entities=[{"type": "bot_command", "offset": 0, "length": 1}],
517+
),
518+
}
519+
)
520+
521+
response = await adapter.handle_webhook(_make_request(body))
522+
assert response["status"] == 200
523+
524+
chat.process_slash_command.assert_not_called()
525+
assert chat.process_message.call_count == 1
397526

398527

399528
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)