Skip to content

feat(masters): add masters update --headline/--text — Этап B point-replacement - #669

Merged
axisrow merged 3 commits into
mainfrom
feat/masters-update-stage-b-665
Aug 2, 2026
Merged

feat(masters): add masters update --headline/--text — Этап B point-replacement#669
axisrow merged 3 commits into
mainfrom
feat/masters-update-stage-b-665

Conversation

@axisrow

@axisrow axisrow commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Also fixes: masters update on DRAFT campaigns (issue #668)

Found while live-verifying this PR — the two DRAFT campaigns available in the account (leftover clones from #659/#663) couldn't be used to test update, because DRAFT edit pages have no "Сохранить кампанию" button at all, only CampaignFormControls.saveDraft.button/.save.button (the latter labelled "Запустить кампанию" — publishes the campaign).

  • update_master now detects DRAFT (via presence of the saveDraft.button testid) and clicks it by default, keeping DRAFT status. New --launch flag publishes instead.
  • Live-recon also found the draft-save click redirects away from /edit/ to the campaign's overview page, and not instantly (~5s observed) — the original immediate post-click reload raced this redirect, producing a false "did not save as requested" error even though the edit had actually saved server-side. Fixed by polling page.url until it leaves /edit/ before re-verifying, mirroring the pattern copy_master already uses for its own post-click redirect.

Test plan

  • pytest tests/test_masters.py -q — 240/240 pass (including 8 new DRAFT-support tests, 7 _set_repeating_value tests, and 14 new update/CLI tests).
  • pytest -q — full offline suite green (2819 passed; the 62 errors are the pre-existing, unrelated VCR/aiohttp environment issue noted in prior masters PRs).
  • black/flake8 clean on all touched files.
  • Live-verified end to end against DRAFT campaign 713231614: replaced headline slot 1 via direct masters update 713231614 --headline "1=...", confirmed saved via reload, reverted to the original text the same way. Confirmed via masters list --status all that the campaign remained DRAFT throughout (never published).

Closes #665
Closes #668

…t-replacement

Implements per-slot headline/ad-text variant replacement for `direct
masters update`, closing the Этап B gap from the #648 umbrella
(originally #631). Reuses `_clear_text_field`/`_read_repeating_values`
from the create-page implementation (#653); does NOT reuse
`_add_repeating_values`, whose contract (clear/rewrite every slot) is
the opposite of a point replacement.

Deliberate departure from this CLI's dominant list-field convention
(`campaigns update --negative-keywords` replaces the whole array) —
Мастер кампаний has no API, variant sets can be large, and forcing
every variant to be re-typed to fix one typo would defeat the point
of a partial update. Documented in code, README, and CHANGELOG as a
conscious choice, not an oversight.

Also fixes `masters update` on DRAFT campaigns (issue #668, found
while live-verifying this PR): DRAFT edit pages have no "Сохранить
кампанию" button at all, only saveDraft/launch — `update_master` now
detects and handles this, defaulting to draft-preserving saves with
an opt-in `--launch`. A live-recon-confirmed redirect race (the
draft-save click navigates away from /edit/ ~5s after clicking,
racing the immediate post-click reload) is fixed by polling page.url
before re-verifying, mirroring copy_master's existing pattern.

Live-verified end to end against DRAFT campaign 713231614: replaced
a headline slot, confirmed saved via reload, reverted to the
original text the same way. Campaign confirmed to remain DRAFT
throughout (masters list --status all).

Closes #665
Closes #668
@axisrow
axisrow force-pushed the feat/masters-update-stage-b-665 branch from 411c928 to 52a6a96 Compare August 2, 2026 09:19
@axisrow

axisrow commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1) — round 39dd6369-7b79-424e-8906-b330df0240ee

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

Verdict Reviewer Finding Location
FIX claude + codex An empty or whitespace-only replacement value blanks an existing ad variant and is reported as a successful update. direct_cli/commands/masters.py:560

Both reviewers independently reached this slot-blanking case. Verified by executing the parser and by reading the post-save comparison: the check tests the re-read slot against the requested value, so a slot that was successfully emptied compares equal and passes. Deleting a variant is documented as out of scope, so the destructive path should be refused rather than silently confirmed.

Tests on the reviewed head: 241 passed. Lint clean on the files this PR touches.

…ant delete)

`masters update --headline "1="` was accepted and silently DELETED the ad
variant in slot 1 instead of replacing it. The whole path was destructive
and reported success:

  _parse_repeating_slot_options split("=", 1) -> {0: ""}
  -> _set_repeating_value clears the slot, types ""
  -> the form is saved (or, with --launch on a DRAFT, published)
  -> _verify_repeating_value_mismatches compares the re-read slot against
     the REQUESTED value: "" == "", so it matches and the delete is
     reported as a successful update.

Deleting a variant is explicitly out of scope for Этап B (#665, "Явно вне
объёма"), and on an active campaign this is a live ad mutation with no
rollback.

Guarded at both layers: click.UsageError at the CLI boundary, before any
browser session opens, and BrowserSessionError in _set_repeating_value
(before the field is read or cleared) so non-CLI callers are safe too.
Whitespace-only values are rejected the same way — same delete in disguise.

Found in review by both reviewers (/review + Codex adversarial).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@axisrow

axisrow commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d4e89bd9b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CHANGELOG.md
Comment on lines +7 to +8
- New `--headline "N=text"` / `--text "N=text"` flags on `direct masters
update <CAMPAIGN_ID>` replace one existing headline/ad-text variant slot

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the documented command on one line

The canonical command is split inside its code span between direct masters and update <CAMPAIGN_ID>. Copying this text into a shell can execute direct masters and update as separate commands, and the repository explicitly forbids multi-line canonical commands in documentation. Keep the complete invocation on one physical line.

AGENTS.md reference: AGENTS.md:L43-L47

Useful? React with 👍 / 👎.

Comment on lines +567 to +570
if slot_number < 1:
raise click.UsageError(
f"{option_name} slot number {slot_number} must be 1 or " "greater."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject oversized slot numbers before opening the browser

When a user passes an out-of-range value such as --headline 6=x or --text 4=x, this parser accepts it, so _with_session opens or authenticates a browser before _set_repeating_value eventually rejects the index. This contradicts the helper's stated fail-fast behavior and makes a purely invalid CLI argument incur browser prompts and surface as a browser-session error rather than a usage error; validate the documented upper bounds here as well.

Useful? React with 👍 / 👎.

…he CLI

`masters update --headline "6=x"` (or `--text "4=x"`) was accepted by
_parse_repeating_slot_options, so a purely invalid CLI argument opened a
browser session — with a possible auth prompt — and only then failed, as a
BrowserSessionError from _set_repeating_value rather than a UsageError.
That contradicts the parser's documented fail-fast contract.

The edit page renders a FIXED 5 headline / 3 text slots, so the bound is
knowable up front. _parse_repeating_slot_options now takes slot_count and
enforces it, with the value imported from the browser layer's own
_HEADLINES_SLOT_COUNT/_TEXTS_SLOT_COUNT so the CLI's bound cannot drift
from the page's.

Below-1 and above-slot_count stay separate branches with distinct messages:
"0=x" is a counting mistake, "6=x" is a slot that does not exist.

Found in review by Codex (P2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant