Skip to content

fix(ads): stop sending implicit Type field in add command#11

Closed
axisrow wants to merge 1 commit into
mainfrom
fix/ads-add-implicit-type-field
Closed

fix(ads): stop sending implicit Type field in add command#11
axisrow wants to merge 1 commit into
mainfrom
fix/ads-add-implicit-type-field

Conversation

@axisrow

@axisrow axisrow commented Apr 10, 2026

Copy link
Copy Markdown
Owner

Summary

Yandex Direct API rejects `ads/add` requests that contain an explicit `"Type"` key inside the Ad object. The actual ad type must be inferred from the presence of `TextAd` / `DynamicTextAd` / `MobileAppAd` sub-objects, NOT declared as a top-level field.

Before this fix the CLI sent:

```json
{"AdGroupId": 12345, "Type": "TEXT_AD", "TextAd": {"Title": "...", ...}}
```

which the API responds to with `InvalidArgumentError` on the `Type` field. After:

```json
{"AdGroupId": 12345, "TextAd": {"Title": "...", ...}}
```

The `--type` CLI option is preserved — it controls which sub-object the CLI builds locally, but it never reaches the wire as a top-level key.

Why this slipped through

`ads add` was the only mutating CLI command that anyone exercised against a real account. The 43 other write commands have zero test coverage and may contain similar bugs. That broader gap is tracked in axisrow/yandex-direct-mcp-plugin#61, which proposes adding dry-run unit tests for every write command (this PR is one of the small foundation pieces of that plan).

Test plan

  • `direct ads add --adgroup-id 1 --type TEXT_AD --title T --text X --href https://example.com --dry-run` → printed JSON no longer contains `"Type"` inside the Ad object
  • (manual, requires sandbox token) `direct --sandbox ads add ... ` → API accepts the request

Related

🤖 Generated with Claude Code

Yandex Direct API rejects requests to ads/add when the body contains
an explicit "Type" key inside the Ad object. The actual ad type must
be inferred from the presence of TextAd / DynamicTextAd / MobileAppAd
sub-objects, NOT declared as a top-level field.

Before this fix the CLI sent:

    {"AdGroupId": <id>, "Type": "TEXT_AD", "TextAd": {...}}

which the API responds to with InvalidArgumentError on the Type field.
Removing it leaves the structure the API actually expects:

    {"AdGroupId": <id>, "TextAd": {...}}

The --type CLI option is preserved — it controls which sub-object the
CLI builds locally, but it never reaches the wire as a top-level key.

This bug shipped to production because ads add was the only mutating
CLI command that anyone exercised against a real account. The 43 other
write commands have zero test coverage and may contain similar bugs.
That broader gap is tracked in axisrow/yandex-direct-mcp-plugin#61.

Refs axisrow/yandex-direct-mcp-plugin#60
Refs axisrow/yandex-direct-mcp-plugin#61

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copilot AI review requested due to automatic review settings April 10, 2026 11:33

Copilot AI 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.

Pull request overview

Fixes direct ads add requests rejected by the Yandex Direct API by stopping the CLI from sending an explicit top-level Type field inside the Ad object, relying instead on the presence of the ad subtype object (e.g., TextAd).

Changes:

  • Remove implicit "Type": ad_type from the ads add request payload, keeping --type as a local builder switch.

Comment on lines 97 to 101
def add(ctx, adgroup_id, ad_type, title, text, href, extra_json, dry_run):
"""Add new ad"""
try:
ad_data = {"AdGroupId": adgroup_id, "Type": ad_type}
ad_data = {"AdGroupId": adgroup_id}

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

This change fixes a wire-format regression; please add a unit test that runs ads add --dry-run and asserts the emitted JSON does not include a top-level Type key in the Ad object. This will prevent reintroducing the invalid ads/add payload in future refactors.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines 99 to 103
try:
ad_data = {"AdGroupId": adgroup_id, "Type": ad_type}
ad_data = {"AdGroupId": adgroup_id}

if ad_type == "TEXT_AD":
ad_data["TextAd"] = {}

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

--type accepts any string, but the implementation only builds TextAd when ad_type == "TEXT_AD". For any other value, the request body will contain only AdGroupId (no ad subtype object) and will be rejected by the API. Consider restricting the Click option to supported values (e.g., click.Choice([...])) and/or raising a clear error when an unsupported type is provided.

Copilot uses AI. Check for mistakes.
axisrow added a commit that referenced this pull request Apr 10, 2026
Yandex Direct API rejects ads/add requests that contain an explicit
top-level "Type" key on the Ad object. The actual ad type must be
inferred from the presence of TextAd / DynamicTextAd / MobileAppAd
sub-objects, NOT declared as a top-level field.

Before this fix the CLI sent:

    {"AdGroupId": 12345, "Type": "TEXT_AD", "TextAd": {...}}

which the API responds to with InvalidArgumentError on Type. After:

    {"AdGroupId": 12345, "TextAd": {...}}

The --type CLI option is preserved — it controls which sub-object the
CLI builds locally, but never reaches the wire as a top-level key.

This bug shipped to production because ads add was the only mutating
CLI command anyone exercised against a real account. The 43 other
write commands have zero test coverage and may contain similar bugs
(see follow-up commits in this same PR for two more confirmed
instances and a dry-run test layer).

Refs axisrow/yandex-direct-mcp-plugin#60
Refs axisrow/yandex-direct-mcp-plugin#61
Supersedes #11

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
axisrow added a commit that referenced this pull request Apr 10, 2026
…un coverage (#12)

Squash-merge of the 4-commit branch fix/test: stops forwarding implicit top-level Type field in ads add / adgroups add / smartadtargets add and update, and adds 32 dry-run payload tests covering every write command with --dry-run support.

Refs axisrow/yandex-direct-mcp-plugin#60
Refs axisrow/yandex-direct-mcp-plugin#61
Supersedes #11
@axisrow

axisrow commented Apr 10, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #12 (merged) — the same ads.py Type fix is included there as the first commit, alongside two additional Type-bug fixes (adgroups, smartadtargets) and 32 regression/dry-run tests covering every write command. Closing as duplicate.

@axisrow axisrow closed this Apr 10, 2026
@axisrow
axisrow deleted the fix/ads-add-implicit-type-field branch April 10, 2026 12:46
axisrow added a commit that referenced this pull request May 30, 2026
* fix(auth): stop --help and unit tests from hanging on client-login network call

A regression from #480 made get_credentials() resolve the bare Client-Login
via a network clients.get on every CLI invocation when an OAuth profile with
an email login lacked the login_migration_checked flag — including plain
`<group> --help`. That call had no timeout (neither the vendored client nor
tapi2 set one), so a slow link or a Yandex SmartCaptcha gateway could hang the
CLI indefinitely. Under CliRunner this also hung the unit suite (it stalled on
test_registered_mapped_groups_show_docs_url, which fires ~30 `--help` calls).

Three independent guards:
- auth.py: cap the best-effort resolver with LOGIN_RESOLVE_TIMEOUT_SECONDS=8
  and disable retries, so it can never wait forever.
- cli.py: skip the resolver entirely on --help/--version/-h passes
  (allow_login_resolve=False) — no command runs, so no login is needed.
- tests/conftest.py: autouse fixture neutralizes the network resolver for the
  whole unit suite; the module-level test-credential probe no longer triggers it.

Adds regression tests asserting a --help pass makes zero resolver calls and
that allow_login_resolve=False suppresses the migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: resolve 13 confirmed bugs from the #483 bug hunt

Verified each finding against the live API, the WSDL cache, and the official
Yandex Direct docs before fixing. Bugs 10 and 14 were false positives
(no UsageError source inside the try — style only) and are intentionally skipped.

Functional fixes:
- bids/keywordbids get: reject empty SelectionCriteria with a UsageError
  before the request (live API error 4001) (#1, #2).
- bids get: re-raise UsageError so the new guard surfaces with exit code 2 (#8).
- bids set-auto: require exactly one of CampaignId/AdGroupId/KeywordId via
  add_single_id_selector (docs: the three are mutually exclusive) (#4).
- vendored to_columns: pad short report rows instead of IndexError (#3).
- vendored error handler: error_data.get("error_detail") to avoid KeyError (#11).
- reports build_report_request: reject empty FieldNames (live API error 8000) (#12).

Swallowed-validation fixes (add `except click.UsageError/ClickException: raise`
before the bare `except Exception`): balance (#6), strategies get (#7),
campaigns get (#9), ads get (#13), negativekeywordsharedsets update (#5).

Type annotation: parse_priority_goals_spec items -> List[Dict[str, Any]] (#15).

build_api_coverage_report: supply --campaign-ids for bids/keywordbids get
wire-capture so the empty-criteria guard does not break schema parity.

Closes #483

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: broaden #483 coverage and add CHANGELOG (cherry-picked from #484)

Incorporates the genuinely-better parts of the parallel PR #484 into this
branch, which already carries the regression tests, the #15 annotation fix,
and the localized messages:

- ads: re-raise click.UsageError in all lifecycle handlers (update, delete,
  archive, unarchive, suspend, resume, moderate) in addition to get — so the
  whole resource matches bug #13's "get + 8 lifecycle" scope. Defensive: these
  handlers have no pre-API UsageError source today, but the guard keeps them
  consistent and future-proof.
- retargeting get/delete, advideos get: same defensive re-raise for uniformity
  (issue #483 items 10/14; not live defects, but harmless consistency).
- bids/keywordbids get: the empty-criteria message now also lists
  --serving-statuses, which likewise populates SelectionCriteria (kept the
  t() localization wrapper).
- CHANGELOG.md: document the bug-hunt fixes (#483) and the --help network-hang
  follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: axisrow <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
axisrow added a commit that referenced this pull request Jun 20, 2026
…wrappers (#589) (#593)

Dedup audit (#589 finding #11, follow-up of #582/epic #584). Eight modules
each defined a near-identical local `_<resource>_lifecycle(method, help_text)`
wrapper purely to repeat `make_lifecycle_command(group, ..., id_param, id_help,
create_client)` for delete/suspend/resume (and archive/unarchive/moderate on
ads/campaigns/strategies). Those eight wrappers collapse into one shared
`register_lifecycle_commands(group, id_param, id_help, create_client, specs)`
in `_lifecycle.py`; each module now passes its `(method, help_text)` spec list.

CLI surface byte-identical — every lifecycle command still registers via the
factory's `@group.command` (the discarded module-level `delete=`/`suspend=`
bindings were unreferenced). Full offline suite green (2536 passed); ruff clean.

`make_set_bids_command` (#12) and `execute_add` (#13) remain in #589.

Co-authored-by: Codex <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
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