Skip to content

fix(accounts): refresh is_premium when a switched-off account is re-enabled (#1315) - #1697

Merged
axisrow merged 1 commit into
axisrow:mainfrom
Pioneer113:fix/1315-account-premium-refresh
Oct 29, 2026
Merged

axisrow merged 1 commit into
axisrow:mainfrom
Pioneer113:fix/1315-account-premium-refresh

Conversation

@Pioneer113

@Pioneer113 Pioneer113 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Why

A Premium account that the operator switched off and then re-authorized gets its is_premium flag 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 fresh Account(is_premium=False) and the upsert writes is_premium=excluded.is_premium, so the flag is reset. accounts.connect then returns early without fetch_me, and accounts.toggle only called add_client on enable. AccountLeasePool routes premium-only work by this flag.

Acceptance criterion

Premium account → operator disables it → re-auth resets is_premium → accounts.connect keeps it out of the pool → accounts.toggle enables it → the account row has is_premium = 1 again. A failing fetch_me on enable is logged and the toggle still succeeds; disabling never touches the flag.

Scope

  • Touched paths: tg_agent/services/dispatcher/accounts_mixin.py (accounts.toggle refreshes the flag right after add_client on enable; the get_client_by_phone → fetch_me → release_client sequence moved into _fetch_premium, shared with accounts.connect), tests/test_telegram_command_dispatcher.py.
  • Explicit exclusions: accounts.connect still 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

  • Tests: commands + results in the table below.
  • Bug fix: 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.
  • New code is actually wired up: the end-to-end test drives _handle_accounts_connect and _handle_accounts_toggle, the same handlers the accounts.* commands dispatch to.
  • CI green: https://github.com/axisrow/tg_content_factory/actions/runs/40381657440
  • Description and criteria reconciled with the final diff.

Verification (Python 3.11.17, branch rebased on main 2a2cdb4):

Check Result
ruff check tg_agent tests packages/tg_shared conftest.py clean
lint-imports --config .importlinter 1 kept, 0 broken
scripts/code_health.py --fail-on F ok
pytest tests -q -m smoke 7 passed, 24 skipped
pytest -q -m "not aiosqlite_serial" -n auto 10263 passed, 152 skipped
pytest tests -q -m aiosqlite_serial -n auto --dist=loadfile 1150 passed
pytest tests/test_telegram_command_dispatcher.py tests/test_telegram_command_dispatcher_structure.py -n auto (this branch alone) 186 passed

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.

…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.

axisrow commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Ревью head 4ab409be0acb38856f9f2f754a77476aa8659a63.

Блокирующих дефектов в просмотренном диффе не нашёл. Сценарий реален: re-auth upsert записывает is_premium=False, disabled connect возвращается раньше запроса Premium, а прежний toggle только подключает клиента. Патч обновляет флаг после включения, не подключая отключённый аккаунт заранее.

Проверены освобождение lease через finally, обработка ошибки fetch как best-effort, сохранение флага при отсутствии клиента и отсутствие Premium-запроса при выключении. Извлечение _fetch_premium сохраняет прежнее поведение активного accounts.connect.

Проверки (Python 3.12):

Ограничение: это проверка DB/dispatcher поведения с моками; она не подтверждает реальный запрос к Telegram или ручной запуск приложения автором.

@axisrow axisrow left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the #1315 follow-up checklist (fix-up to the #1661 review):

  • Runtime split: _fetch_premium does make a live Telegram call (get_client_by_phone → fetch_me), but both call sites (_handle_accounts_connect, _handle_accounts_toggle) are TelegramCommandDispatcher handlers, which web/bootstrap.py constructs only in runtime_mode == "worker"; the web route (/settings/accounts/{id}/toggle) only enqueues accounts.toggle into telegram_commands. The web side still never opens Telegram connections. Consistent.
  • Write path: AccountsRepository.update_account_premium goes through db.execute_write (#569 locked-write convention). No raw commits anywhere in the diff.
  • Races: flood-waited phone → get_client_by_phone defaults to wait_for_flood=False, returns None → no write, toggle still succeeds (flag self-heals on the next worker restart, which ClientPool.initialize also refreshes); session-less/degraded accounts never reach add_client (load_live_usable_accounts skips them, existing warning path) → no refresh, no crash; fetch_me failure releases the lease in finally and is swallowed as best-effort, matching the acceptance criterion. The premise also checks out: the re-auth upsert writes is_premium=excluded.is_premium, so the reset is real.
  • Tests: test_reenabling_reauthed_disabled_account_restores_premium is 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_connect refactor 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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@axisrow
axisrow merged commit 4c5c504 into axisrow:main Oct 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants