fix(masters): retry no-op action-button click, calibrate status timeout (#766, #764) - #771
Merged
Merged
Conversation
…ut (#766, #764) `masters suspend`/`resume` never changed the campaign's status and failed by timeout. Live diagnosis (2026-08-06, campaign 713277109) found the cause is not a missed selector nor a too-small timeout: the FIRST click on a freshly rendered overview page is frequently a silent no-op — Playwright's actionability checks pass, `.click()` returns without raising, and no network request is issued at all, because React's own click handler was not yet attached. Waiting longer can never fix that state, which is why retrying the CLI command never helped and why #758's 60s budget only made each failure slower. - Retry the action-button click (`_STATUS_CLICK_MAX_ATTEMPTS`, 4), checking the status before each attempt so an effective click is never repeated. Same for the unarchive->SUSPENDED leg of resume. - Lower `_STATUS_CHANGE_TIMEOUT_MS` 60s -> 8s, now measured: over 12 real transitions the lag from an effective click to the status text updating was 1.64-2.28s (mean 1.8s), no long tail (#764). - Resolve both buttons via live-confirmed testids (`CampaignHeader.ActionButton.stop`/`.resume`) instead of guessed Russian labels; keep the labels as a fallback that now resolves the enclosing <button> before clicking. - Poll for a recognised status instead of reading once — a single read live-aborted a real suspend with "unrecognised status text". - Don't abort the batch at the first failing ID: attempt every ID, report each outcome, exit non-zero afterwards. `suspend`/`resume`/`launch`/ `archive` now share one `_run_per_id` helper. - Name the selector, the labels searched, and what the page actually renders in the "could not find the button" error. Live-verified end to end against campaign 713277109 (suspend, resume, mixed-ID batch); returned to its original ACTIVE state afterwards. Closes #766 Closes #764 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H564VvyTSMJQ8BreL9bMn5
The 60s -> 8s recalibration in #764 measured one quantity (the lag from an effective click to the status text updating, 1.64-2.28s over 12 live transitions) but was applied to two. `_wait_for_recognised_status` waits for the status element to render at all after navigating -- a different, never measured quantity that ran on the pre-#766 60s budget inline in `resume_master` before this branch folded it into the shared constant. That path branches: a campaign whose ARCHIVED status has not hydrated within the budget reads as None, skips the unarchive step, and then hunts for a resume button an archived page never renders -- leaving the campaign archived behind a misleading "could not find the button" error. Give it its own `_STATUS_HYDRATION_TIMEOUT_MS`, keeping the pre-#766 60s until a real measurement replaces it. The measured 8s stays where it was measured, post-click. Also add a temporary, off-by-default timing scaffold: with DIRECT_MASTERS_DEBUG_TIMING=1 each wait prints its actual duration and the click count to stderr, so the remaining unmeasured budget gets set from live runs instead of guessed a third time. Deliberately minimal -- one env var read in one function, three call sites, no CLI flag and no logging config -- so removing it is a clean delete. The new guard test asserts the two budgets stay separate; mutation-checked by collapsing them, which turns it red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H564VvyTSMJQ8BreL9bMn5
…d button The retry loop clicked, polled, and — on a non-target read — clicked again with no status read in between, while the module docstring claimed it was "checking the status before each attempt so an already-effective click is never repeated". The poll has a deadline, so that claim only held while the status update arrived inside the 8s window. Past it, the second click is actively harmful: for `suspend` it toggles the campaign straight back to ACTIVE, and for `resume` the page has already swapped `CampaignHeader.ActionButton.resume` for `.stop`, so the re-click matches neither the testid nor the fallback labels and raises "could not find an action button" -- reporting a hard failure for a mutation that succeeded, which `_run_per_id` then surfaces as "Failed to resume 1 of N". Re-read the status immediately before every retry (both the action-button loop and the unarchive menu-item loop), and treat a button that vanished between that read and the click as success when the status already reached its target. Two tests cover the two guards, each mutation-checked by removing the guard it covers. Also routes this branch's poll deadlines through `_clock.now()` (#767/#770, merged into main meanwhile) as the PR description anticipated, and gives `_wait_for_recognised_status` the hydration budget rather than the post-click one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H564VvyTSMJQ8BreL9bMn5
axisrow
force-pushed
the
fix/766-764-suspend-timeout-and-click
branch
from
August 6, 2026 05:13
0e4465b to
e7eec86
Compare
…imeout-and-click # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что было
direct masters suspendне останавливал Мастер-кампании: клик не менял статус, команда падала по таймауту 60 с, retry не помогал (#766). Параллельно #764 просил измерить реальную задержку смены статуса вместо взятой наугад константы.Что нашла живая разведка (2026-08-06, кампания 713277109)
Оба issue — один корень, и это не то, что предполагалось ни в одном из них:
Первый клик по кнопке на свежеотрисованной странице обзора — регулярно молчаливый no-op. Проверки actionability проходят,
.click()возвращается без исключения, и — подтверждено перехватом всех запросов после клика — не уходит вообще ни одного сетевого запроса. React ещё не навесил свой обработчик. Никакое ожидание это состояние не чинит: поэтому повторный запуск команды не помогал, а 60-секундный бюджет из #758 лишь замедлял каждый отказ.Промаха по подписи кнопки (гипотеза #766 со ссылкой на #630) не было:
"Остановить кампанию"матчилась. Зато нашлись стабильные testid'ы, снятые с живого DOM.Замер для #764 — 12 реальных переходов в двух прогонах: от результативного клика до обновления статуса на странице 1.64–2.28 с (среднее 1.8 с), без длинного хвоста. 60 с никогда не измеряли эту задержку — они маскировали баг выше.
Что сделано
_STATUS_CLICK_MAX_ATTEMPTS= 4) со сверкой статуса перед каждой попыткой — результативный клик никогда не повторяется. То же для шага unarchive→SUSPENDED вresume. Это ровно то лечение, которое_click_and_wait_for_popupуже применял к «⋮»-меню и модалке переименования (masters copy: клик по меню-триггеру race — 'Клонировать' не находится сразу после _goto_overview_page #723/masters update --name: клик по карандаш-кнопке race, модалка переименования не открывается #725); suspend/resume его просто никогда не получал._STATUS_CHANGE_TIMEOUT_MS: 60 с → 8 с, теперь измеренное значение (~3.5× наблюдённого максимума).CampaignHeader.ActionButton.stop/.resume. Текстовые подписи остались фолбэком и теперь резолвят объемлющий<button>перед кликом —get_by_textматчит<span class="dc-Button__text">внутри, из-за чего проверкиdisabled/aria-disabledраньше делались не по тому элементу.masters suspendошибкой «unrecognised status text» на кампании, чей статус читался секундой позже.Error), ненулевой exit — только после печати всех исходов. Так уже вели себяlaunch/archive(feat(masters): adddirect masters archive(#633) #645); теперь все четыре команды используют общий_run_per_id.Верификация
Живьём на кампании 713277109 (по согласованному слоту):
masters resume(SUSPENDED→MODERATION)masters suspend(ACTIVE→SUSPENDED)713277109,999999999Офлайн: 3238 passed, 23 skipped (весь сьют),
black/flake8чисто по изменённым файлам. Добавлено 8 регрессионных тестов: no-op-ретрай, отсутствие повторного клика после успеха, приоритет testid над текстом, клик по<button>а не<span>, содержимое ошибки, поллинг статуса, батч-устойчивость для suspend и resume.Замечание к мержу
Ветка не содержит
direct_cli/browser/_clock.pyиз #767/PR #770 (параллельный воркер). Мои poll-циклы используютtime.monotonic()— как весь остальной модуль на этой ветке. После мержа #770 их стоит перевести на_clock.now(); снижение таймаута 60с→8с само по себе уже убирает основную часть busy-spin в этих тестах.Closes #766
Closes #764
🤖 Generated with Claude Code
https://claude.ai/code/session_01H564VvyTSMJQ8BreL9bMn5