Repository navigation
fix(accounts): refresh is_premium when a switched-off account is re-enabled (#1315) - #1697
Conversation
…nabled (axisrow#1315) Re-auth of an account the operator disabled upserts the fresh Account, which resets is_premium to False, and accounts.connect then returns early without fetch_me (it must not bring the account into the live pool). accounts.toggle added the client back on enable but never read the flag again, so a Premium account stayed non-premium until the next worker restart. accounts.toggle now refreshes is_premium (best effort) right after add_client on enable; the fetch_me lookup is shared with accounts.connect. Follow-up to the axisrow#1661 review.
|
Ревью head Блокирующих дефектов в просмотренном диффе не нашёл. Сценарий реален: re-auth upsert записывает Проверены освобождение lease через finally, обработка ошибки fetch как best-effort, сохранение флага при отсутствии клиента и отсутствие Premium-запроса при выключении. Извлечение Проверки (Python 3.12):
Ограничение: это проверка DB/dispatcher поведения с моками; она не подтверждает реальный запрос к Telegram или ручной запуск приложения автором. |
axisrow
left a comment
There was a problem hiding this comment.
Reviewed against the #1315 follow-up checklist (fix-up to the #1661 review):
- Runtime split:
_fetch_premiumdoes make a live Telegram call (get_client_by_phone→fetch_me), but both call sites (_handle_accounts_connect,_handle_accounts_toggle) areTelegramCommandDispatcherhandlers, whichweb/bootstrap.pyconstructs only inruntime_mode == "worker"; the web route (/settings/accounts/{id}/toggle) only enqueuesaccounts.toggleintotelegram_commands. The web side still never opens Telegram connections. Consistent. - Write path:
AccountsRepository.update_account_premiumgoes throughdb.execute_write(#569 locked-write convention). No raw commits anywhere in the diff. - Races: flood-waited phone →
get_client_by_phonedefaults towait_for_flood=False, returnsNone→ no write, toggle still succeeds (flag self-heals on the next worker restart, whichClientPool.initializealso refreshes); session-less/degraded accounts never reachadd_client(load_live_usable_accountsskips them, existing warning path) → no refresh, no crash;fetch_mefailure releases the lease infinallyand is swallowed as best-effort, matching the acceptance criterion. The premise also checks out: the re-auth upsert writesis_premium=excluded.is_premium, so the reset is real. - Tests:
test_reenabling_reauthed_disabled_account_restores_premiumis fake-level (real file DB + real handlers, mocked pool) and drives both handlers end-to-end; mock-level tests cover the refresh write, the best-effort failure and the disable path. The_handle_accounts_connectrefactor is behavior-preserving (bool(None)→False, same as the old default).
Two non-blocking nits inline; no blockers from my side.
| ) | ||
| else: | ||
| await self._pool.add_client(live_account.phone, live_account.session_string) | ||
| await self._refresh_premium_after_enable(live_account) |
There was a problem hiding this comment.
Nit, non-blocking: this call sits inside the same try/except as add_client, so if the best-effort update_account_premium write itself fails, it gets logged with the misleading label "failed to add client". Behavior is fine (toggle still succeeds); either catch around the refresh or reword that log line whenever you next touch this file.
| db.get_accounts.return_value = [Account(id=2, phone="+2", session_string="s", is_active=False)] | ||
| session = MagicMock() | ||
| session.fetch_me = AsyncMock(return_value=MagicMock(premium=True)) | ||
| pool.get_client_by_phone = AsyncMock(return_value=(session, "+2")) |
There was a problem hiding this comment.
Nit, non-blocking: the flood-waited branch — get_client_by_phone returns None → no premium write, toggle still succeeds — is untested in the toggle context. One more mock test (get_client_by_phone → None, assert update_account_premium not awaited and is_active True) would pin the wait_for_flood=False default this relies on.
Why
A Premium account that the operator switched off and then re-authorized gets its
is_premiumflag back when it is switched on again, instead of staying non-premium until the next worker restart. Follow-up to the #1661 review (#1315), item 3: "_handle_accounts_connectпри раннем возврате для выключенного аккаунта пропускает best-effort-обновлениеis_premium".Re-auth (
auth.verify_code, QR) upserts a freshAccount(is_premium=False)and the upsert writesis_premium=excluded.is_premium, so the flag is reset.accounts.connectthen returns early withoutfetch_me, andaccounts.toggleonly calledadd_clienton enable.AccountLeasePoolroutes premium-only work by this flag.Acceptance criterion
Premium account → operator disables it → re-auth resets
is_premium→accounts.connectkeeps it out of the pool →accounts.toggleenables it → the account row hasis_premium = 1again. A failingfetch_meon enable is logged and the toggle still succeeds; disabling never touches the flag.Scope
tg_agent/services/dispatcher/accounts_mixin.py(accounts.togglerefreshes the flag right afteradd_clienton enable; theget_client_by_phone→fetch_me→release_clientsequence moved into_fetch_premium, shared withaccounts.connect),tests/test_telegram_command_dispatcher.py.accounts.connectstill does not open a Telegram session for a disabled account (that is the provenance: аккаунты, модерация, планировщик (этап 6) #1315 rule), and its behaviour for active accounts is unchanged; the re-auth upsert is not changed.Evidence
test_reenabling_reauthed_disabled_account_restores_premium(real DB, worker command handlers) was written first and failed on the original code; mock-level tests cover the refresh, the best-effort failure and the disable path._handle_accounts_connectand_handle_accounts_toggle, the same handlers theaccounts.*commands dispatch to.Verification (Python 3.11.17, branch rebased on
main2a2cdb4):ruff check tg_agent tests packages/tg_shared conftest.pylint-imports --config .importlinterscripts/code_health.py --fail-on Fpytest tests -q -m smokepytest -q -m "not aiosqlite_serial" -n autopytest tests -q -m aiosqlite_serial -n auto --dist=loadfilepytest tests/test_telegram_command_dispatcher.py tests/test_telegram_command_dispatcher_structure.py -n auto(this branch alone)The full-suite rows are one run over
main+ all three #1661 follow-up branches together (they touch disjoint files); the last row is this branch on its own.