Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ This directory holds implementation notes for shipped features and feature fixes
- [Docker Hub publish pipeline](dockerhub-publish-pipeline.md)
- [Menu label alias routing](menu-label-alias-routing.md)
- [Song artist and source URL split](song-artist-source-url-split.md)
- [Start command recovery and copy fixes](start-command-recovery-and-copy-fixes.md)
- [Ukrainian-only bot localization](ukrainian-localization.md)
37 changes: 37 additions & 0 deletions docs/features/start-command-recovery-and-copy-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Start Command Recovery and Copy Fixes

## Summary

This update addresses three UX bugs in the Ukrainian Telegram bot flow:

- Song detail copy now labels source as `Джерело (оригінал)`.
- The chart-key optional step now shows both `Пропустити` and `Скасувати`.
- `/start` is restored in Telegram command surfaces for easier recovery after chat history cleanup.

## What changed

- Updated the shared song detail formatter to render:
- `Джерело (оригінал): ...`
- Updated chart upload prompt keyboard for optional chart key:
- changed from cancel-only keyboard to skip+cancel keyboard.
- Updated bot startup command publication:
- publish only one command: `/start` with description `Відкрити головне меню`
- set global chat menu button to Telegram commands menu (`MenuButtonCommands`)

## Handler and startup impact

- `handlers/repertoire.py`
- copy-only change in `format_song` output.
- `handlers/charts.py`
- reply markup change for the chart-key prompt; no state-machine or parsing changes.
- `bot/application.py`
- replaced `delete_my_commands()` with explicit command publishing and commands menu button setup.

## Testing

- Updated handler tests to assert:
- new source label text in detailed song cards
- presence of both `Пропустити` and `Скасувати` buttons at chart-key step
- Updated application startup test to assert:
- `/start` command is published
- chat menu button is set to `MenuButtonCommands`
5 changes: 3 additions & 2 deletions src/bot/application.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from telegram import Update
from telegram import BotCommand, MenuButtonCommands, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters

from bot.runtime import (
Expand Down Expand Up @@ -29,7 +29,8 @@ async def post_init(application: Application) -> None:
chart_service = application.bot_data.get(CHART_SERVICE_KEY)
if isinstance(chart_service, ChartService):
await chart_service.ensure_storage_ready()
await application.bot.delete_my_commands()
await application.bot.set_my_commands([BotCommand("start", "Відкрити головне меню")])
await application.bot.set_chat_menu_button(menu_button=MenuButtonCommands())


async def post_shutdown(application: Application) -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
user_state,
)
from handlers.messages import NEXT_ACTIONS_MESSAGE
from handlers.ui import BUTTON_SKIP, cancel_markup
from handlers.ui import BUTTON_SKIP, cancel_markup, skip_cancel_markup
from services.chart_service import ChartFile, ChartUpload, SongChartNotFoundError
from services.song_service import SongNotFoundError
from storage.chart_storage import ChartStorageError
Expand Down Expand Up @@ -173,7 +173,7 @@ async def upload_chart_media(update: Update, context: ContextTypes.DEFAULT_TYPE)
state["filename"] = filename
await update.effective_message.reply_text(
"Тональність гармонії необов'язкова. Надішліть текст або «Пропустити».",
reply_markup=cancel_markup(update),
reply_markup=skip_cancel_markup(update),
)
return UPLOAD_CHART_KEY

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/repertoire.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def format_song(song: Song) -> str:
return (
f"#{song.id} {song.title}\n"
f"Виконавець: {song.artist}\n"
f"Джерело: {source_text}\n"
f"Джерело (оригінал): {source_text}\n"
f"Тональність: {song.key}\n"
f"Каподастр: {capo_text}\n"
f"Розмір: {time_signature_text}\n"
Expand Down
18 changes: 15 additions & 3 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@
from unittest.mock import AsyncMock

import pytest
from telegram import MenuButtonCommands

from bot.application import post_init


@pytest.mark.asyncio
async def test_post_init_clears_published_bot_commands() -> None:
async def test_post_init_publishes_start_command_and_commands_menu_button() -> None:
application = SimpleNamespace(
bot_data={},
bot=SimpleNamespace(delete_my_commands=AsyncMock()),
bot=SimpleNamespace(
set_my_commands=AsyncMock(),
set_chat_menu_button=AsyncMock(),
),
)

await post_init(application)

application.bot.delete_my_commands.assert_awaited_once_with()
application.bot.set_my_commands.assert_awaited_once()
commands = application.bot.set_my_commands.await_args.args[0]
assert len(commands) == 1
assert commands[0].command == "start"
assert commands[0].description == "Відкрити головне меню"

application.bot.set_chat_menu_button.assert_awaited_once()
menu_button = application.bot.set_chat_menu_button.await_args.kwargs["menu_button"]
assert isinstance(menu_button, MenuButtonCommands)
9 changes: 6 additions & 3 deletions tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
list_songs_command,
search_songs_command,
)
from handlers.ui import BUTTON_CANCEL, MENU_START
from handlers.ui import BUTTON_CANCEL, BUTTON_SKIP, MENU_START
from models.song import Song, SongStatus
from services.chart_service import SongChartNotFoundError
from services.repertoire_backup_service import BackupArchive
Expand Down Expand Up @@ -291,7 +291,7 @@ async def test_list_songs_command_sends_detailed_song_cards_when_result_fits() -
reply.assert_awaited_once()
message = reply.await_args.args[0]
assert "Виконавець: Traditional" in message
assert "Джерело: -" in message
assert "Джерело (оригінал): -" in message
assert "Нотатки: Slow intro." in message
assert "Каподастр: 1" in message
assert "Розмір: 3/4" in message
Expand All @@ -311,7 +311,7 @@ async def test_search_command_sends_detailed_song_cards_when_result_fits() -> No
reply.assert_awaited_once()
message = reply.await_args.args[0]
assert "Виконавець: Traditional" in message
assert "Джерело: -" in message
assert "Джерело (оригінал): -" in message
assert "Нотатки: Slow intro." in message
assert "Каподастр: 1" in message
assert "Розмір: 3/4" in message
Expand Down Expand Up @@ -458,6 +458,9 @@ async def test_upload_chart_media_moves_directly_to_chart_key_step() -> None:
assert reply.await_args.args[0] == (
"Тональність гармонії необов'язкова. Надішліть текст або «Пропустити»."
)
keyboard = reply.await_args.kwargs["reply_markup"]
assert keyboard.keyboard[0][0].text == BUTTON_SKIP
assert keyboard.keyboard[0][1].text == BUTTON_CANCEL


@pytest.mark.asyncio
Expand Down
Loading