Skip to content

fix(masters): read status from CampaignHeader.Status, not whole-page text (#848) - #849

Merged
axisrow merged 3 commits into
mainfrom
ao/direct-cli-105/masters-status-header
Aug 13, 2026
Merged

fix(masters): read status from CampaignHeader.Status, not whole-page text (#848)#849
axisrow merged 3 commits into
mainfrom
ao/direct-cli-105/masters-status-header

Conversation

@axisrow

@axisrow axisrow commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Проблема

masters archive и masters suspend отказывались работать с кампанией 107705868:

Error: Could not determine current status for campaign 107705868 (unrecognised status text)
— refusing to click blind. The page reports: action buttons on the page:
'CampaignHeader.ActionButton.stop' ('Остановить кампанию', enabled); page status reads as None.

При этом сама диагностика в тексте ошибки сообщала, что кнопка «Остановить кампанию» отрисована и enabled — то есть кампания заведомо ACTIVE. Отказ сам по себе корректен (лучше не кликать вслепую), но кампания оказывалась недоступна для всех lifecycle-команд.

Причина

_read_status_text искал четыре фиксированные русские фразы как подстроки в inner_text("body") — по всей странице целиком.

Живая разведка 2026-08-14 показала, что CampaignHeader.Status содержит статус дословно во всех состояниях, а не только у DRAFT, как предполагали старые комментарии модуля. Подтверждено на реальных кампаниях:

Кампания CampaignHeader.Status Статус
713234142 Кампания активна ACTIVE
107705868 Кампания остановлена SUSPENDED
100571135 Кампания в\xa0архиве ARCHIVED

Решение

Читать этот элемент первым; откат на body-текст — только если элемент отсутствует или не распознан.

Это убирает сам класс отказа «подстрока по всей странице»: дашборд рендерит свободный текст модерации, упоминающий кампанию (подтверждено живьём на 713234142: «…Кампания запущена из\xa0прошедших модерацию элементов объявления»), поэтому какой статус «победит», зависело от того, какие посторонние баннеры отрисовались.

Сравнение маркеров дополнительно нормализуется по регистру, U+00A0 и схлопыванию пробелов — маркер больше не может молча не сматчиться из-за невидимого символа. Эта ловушка уже дважды стоила отладочного прохода (#704, #730).

Нераспознанный текст по-прежнему даёт None, так что защита refuse-to-click-blind не ослаблена.

Проверка

  • Живая верификация всех трёх состояний на реальном аккаунте (ACTIVE / SUSPENDED / ARCHIVED) — статус читается корректно.
  • 7 новых регрессионных тестов (TestReadStatusText); проверено, что без фикса 5 из них падают.
  • pytest — 3626 passed, 23 skipped. black + flake8 чисто.

Остаётся пользователю

Кампания 107705868 всё ещё не заархивирована — живая production-мутация вне области фикса и требует ручного запуска masters archive 107705868.

Closes #848

🤖 Generated with Claude Code

https://claude.ai/code/session_018f6rRmUxj7Y33jfzcok8gC

…text (#848)

`masters archive`/`suspend` refused to act on campaign 107705868 with
"unrecognised status text", while the same error's own diagnostic reported
`CampaignHeader.ActionButton.stop` as present and enabled — i.e. the campaign
was plainly ACTIVE. The refusal is correct behaviour, but it left the campaign
unreachable by every lifecycle command.

`_read_status_text` matched four fixed Russian phrases as substrings of
`inner_text("body")`. Live recon 2026-08-14 established `CampaignHeader.Status`
carries the status verbatim in every state, not just DRAFT as the module's older
comments assumed — confirmed on real campaigns: "Кампания активна" (713234142),
"Кампания остановлена" (107705868), "Кампания в архиве" (100571135). Read that
element first; fall back to body text only when absent or unrecognised.

This removes the substring-over-everything failure mode: the dashboard renders
free-form moderation prose mentioning the campaign (confirmed live on 713234142),
so which status won depended on which unrelated banners rendered. Marker
comparison is also normalised over case, U+00A0 and collapsed whitespace, so a
marker can no longer silently never match over an invisible character — a pitfall
that already cost a debugging pass twice (#704, #730).

Unrecognised text still returns None, so the refuse-to-click-blind guard is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f6rRmUxj7Y33jfzcok8gC
@axisrow

axisrow commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1) — round 1a36be67-1cf9-4eed-8ecd-d5fe2914516e

Reviewed locally (/review + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
FIX codex + claude Header-status probe can block ~30s on an absent element, defeating the callers 8s poll budget; no bounded timeout is set. direct_cli/browser/masters.py:2667-2674
SKIP claude Marker ordering comment claims a substring-shadow risk that does not exist among the current marker strings; cosmetic only. direct_cli/browser/masters.py:2685-2696
FIX claude A recognised-but-unmatched header status still falls back to whole-body text matching, reopening the exact unrelated-banner false-positive risk the fix set out to close. direct_cli/browser/masters.py:2637-2664

…ader body fallback (#848 review)

Two issues found in local cycle-review (Codex + built-in /review) of
PR #849's header-first status fix:

- _read_campaign_header_status_text's inner_text() had no explicit
  timeout, so an absent/not-yet-rendered CampaignHeader.Status element
  paid Playwright's ~30s default wait — defeating the much shorter
  8s _STATUS_CHANGE_TIMEOUT_MS poll budget used by every caller (up to
  4 retries via _STATUS_CLICK_MAX_ATTEMPTS). Now passes timeout=1_000,
  matching this file's existing probe-read convention.

- _read_status_text fell through to whole-body matching whenever the
  header text didn't match a known marker, even though the header had
  been read successfully — reopening the exact substring-over-everything
  failure mode #848's fix was meant to close, just for an unrecognised
  header string instead of a missing one. Now only falls back to body
  text when the header element itself is absent.

7 new/updated regression tests in TestReadStatusText and
TestFetchMasterDraft; full suite green (3628 passed, 23 skipped),
black+flake8 clean on changed files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f6rRmUxj7Y33jfzcok8gC
@axisrow

axisrow commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 2) — round ba4542d2-bf1b-485f-8d00-6e3aa225d0b8

Reviewed locally (/review + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
FIX codex + claude A present-but-slow-hydrating header element is currently indistinguishable from an absent one, so a transient read timeout can still trigger the unreliable whole-body fallback on a live mutation-polling path. direct_cli/browser/masters.py (_read_status_text / _read_campaign_header_status_text)

 round-2 review)

Round-1's fix conflated two different meanings of a None read from
_read_campaign_header_status_text: 'this page shape structurally has
no CampaignHeader.Status element' (safe to fall back to body text)
and 'the element is present but its read raised/timed out mid-
hydration' (a transient state — the exact window callers are polling
through on a status transition). Both reviewers (Codex + built-in
/review) flagged that a timeout during header hydration could still
trip the unreliable whole-body substring fallback, on a live mutation
polling path, reopening a narrower form of the bug #848 fixed.

_read_status_text now checks Locator.count() (synchronous, no wait)
before reading the header: count() == 0 means structurally absent and
falls back to body text as before; count() > 0 means present, so a
failed/unrecognised read returns None directly rather than trying
body text — consistent with the existing refuse-to-click-blind
philosophy, with callers' own poll loops retrying next tick.

2 new regression tests; full suite green (3630 passed, 23 skipped),
black+flake8 clean on changed files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f6rRmUxj7Y33jfzcok8gC
@axisrow

axisrow commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 3) — round 128e69e8-bdf1-4147-8f85-2b23b0907c7e

Reviewed locally (/review + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
codex Codex approved with zero findings, confirming the header-first fail-closed design bounds probe latency and avoids noisy body fallback. (approve, no findings)
SKIP claude A present-but-timed-out header correctly returns None rather than falling back to body text by explicit round-2 design intent, not an oversight. direct_cli/browser/masters.py:_read_status_text
SKIP claude Re-raised without new evidence — already explicitly declined last round as a harmless, if slightly overstated, comment. direct_cli/browser/masters.py:_STATUS_TEXT_MARKERS
SKIP claude A harmless double-query the reviewer itself already flagged as a non-issue. direct_cli/browser/masters.py:_read_campaign_header_status_text

No FIX verdicts this round — cycle complete.

@axisrow

axisrow commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

📋 Review summary — all cycles

Cycle Reviewer Finding Verdict Resolution
1 codex + claude Header-status probe blocked ~30 seconds on an absent element by using no timeout, defeating the 8-second poll budget the callers rely on. FIX Fixed in c90a7e6 (bounded timeout=1_000)
1 claude Marker ordering comment claims a substring-shadow risk that does not exist among the five current marker strings; cosmetic only. SKIP Left as-is — no functional bug
1 claude A header status text that was read successfully but matched no known marker still fell back to whole-body substring matching, reopening the exact unrelated-banner false-positive risk the fix was meant to close. FIX Fixed in c90a7e6 (header authoritative once read)
2 codex + claude A header element that is present but whose read times out mid-hydration was treated the same as a structurally absent element, letting a transient hydration race still trip the unreliable body-text fallback on a live mutation-polling path. FIX Fixed in 4c86ccd (count()-based absent/timed-out distinction)
3 claude A present-but-timed-out header correctly returns no status rather than guessing from body text, by explicit design — not an oversight, as a later review pass mischaracterized it. SKIP Confirmed intentional round-2 design, not a gap
3 claude Marker ordering comment claims a substring-shadow risk that does not exist among the five current marker strings; cosmetic only. (repeat) SKIP No new evidence since cycle 1's decision
3 claude A harmless double query against the same DOM selector, already self-flagged by its own reviewer as not a defect. SKIP Reviewer's own non-issue
3 codex (approve, no findings) Header-first parsing confirmed fail-closed

Totals: 3 FIX (all resolved across 2 commits), 5 SKIP, 0 UNVERIFIED.

Both reviewers (/review + Codex companion) returned a clean round 3 with no FIX/UNVERIFIED verdicts — cycle complete. Per this skill's review-only-on-merge policy, merge remains a manual step.

@axisrow
axisrow merged commit 517eb62 into main Aug 13, 2026
6 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.

masters archive/suspend отказывают на кампании 107705868: unrecognised status text

1 participant