Skip to content

test(wsdl): system parity gate for mutating CLI commands (#198)#205

Merged
axisrow merged 3 commits into
mainfrom
worktree-milestone-0.3.9-pr1-wsdl-gate
May 20, 2026
Merged

test(wsdl): system parity gate for mutating CLI commands (#198)#205
axisrow merged 3 commits into
mainfrom
worktree-milestone-0.3.9-pr1-wsdl-gate

Conversation

@axisrow

@axisrow axisrow commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

PR 1 of milestone 0.3.9. Adds tests/test_wsdl_parity_gate.py — a system-level WSDL ↔ CLI parity gate covering the four recurring bug patterns identified in the issue #198 audit. Subsequent PRs (PR 2) flip the xfailed tests to passing by fixing the underlying bugs.

This is the infrastructure PR — it adds the gate first so the bug fixes that follow are caught by automation rather than by hand.

What the gate checks

Pattern Trigger Status today
A — Empty subtype no-op bids set --keyword-id 5 --dry-run prints {KeywordId:5} instead of refusing 5 xfailed
B — Silent data loss ads update --type TEXT_AD --image-hash X drops --image-hash silently 1 xfailed
C — WSDL minOccurs=1 unvalidated adgroups add accepts no --region-ids although WSDL requires it 1 xfailed (RegionIds), rest passing
D — Strategy enum drift STRATEGY_TYPES has 5 bogus names + misses 5 WSDL names 1 xfailed

Files

  • direct_cli/wsdl_coverage.py — adds get_required_fields() and get_required_item_fields() helpers on top of get_operation_request_schema().
  • tests/test_wsdl_parity_gate.py — 45 parametrized tests across every WRITE_SANDBOX add/update/set command.
  • CLAUDE.md — Key Conventions entry describing how to extend COMMAND_WSDL_MAP and WSDL_FIELD_TO_CLI_OPTION when adding a new mutating command.

Test plan

  • pytest -q tests/test_wsdl_parity_gate.py → 29 passed, 8 skipped, 8 xfailed
  • pytest -q tests/test_api_coverage.py tests/test_cli.py tests/test_comprehensive.py tests/test_dry_run.py tests/test_wsdl_parity_gate.py → 354 passed
  • flake8 tests/test_wsdl_parity_gate.py clean
  • black tests/test_wsdl_parity_gate.py clean
  • CI green

Next

PR 2 fixes the 12 still-actual High bugs from #198 — the xfailed tests above flip green at that point, no gate logic touches.

Refs #198. Part of milestone 0.3.9.

🤖 Generated with Claude Code

Adds tests/test_wsdl_parity_gate.py — a four-pattern gate across every
add/update/set command in WRITE_SANDBOX. Each pattern targets one of
the recurring bug classes found by the issue #198 systemic audit:

  A. Empty subtype no-op — bid/keyword/ad update with only the ID must
     refuse to send (silent no-op on the live API). 5 xfailed probes.
  B. Silent data loss — incompatible flag for chosen --type must raise
     UsageError instead of being dropped. 1 xfailed probe.
  C. WSDL minOccurs=1 unvalidated — required item fields must be
     enforced via Click required=True OR a documented body-level
     UsageError (INTERNAL_VALIDATION whitelist with smoke probes).
     adgroups.add/RegionIds remains xfailed pending PR 2.
  D. Strategy enum drift — STRATEGY_TYPES in strategies.py must equal
     the choice-of-one subtypes in StrategyAddItem. xfailed pending PR 2.

Adds two helpers in wsdl_coverage.py:
  - get_required_fields(schema) for top-level minOccurs >= 1 fields
  - get_required_item_fields(schema, container) for nested item fields

Adds CLAUDE.md guidance for extending COMMAND_WSDL_MAP /
WSDL_FIELD_TO_CLI_OPTION when a new mutating command is added.

Coverage: 29 passed, 8 skipped (no required item fields), 8 xfailed
(known issue #198 bugs to be flipped green by PR 2).

Refs #198

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@axisrow axisrow added this to the 0.3.9 milestone May 20, 2026
@axisrow

axisrow commented May 20, 2026

Copy link
Copy Markdown
Owner Author

@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production.

@claude

claude Bot commented May 20, 2026

Copy link
Copy Markdown

Claude finished @axisrow's task in 5m 40s —— View job


Review — focused on critical issues only

I read the full diff against origin/main, the new gate, the updated wsdl_coverage.py helpers, the smoke matrix (WRITE_SANDBOX vs DANGEROUS), and confirmed the INTERNAL_VALIDATION expected substrings against the actual UsageError strings in direct_cli/commands/*.py.

Bottom line: no production-impacting bugs, security issues, data-loss risks, or performance problems. The gate is test-only (read-path) and the helpers added to wsdl_coverage.py are pure XML traversal. Below are the items I'd consider before relying on this gate as the green-light for PR 2.


1. xfail tests only assert exit_code != 0 — risk of green-for-the-wrong-reason in PR 2

test_empty_payload_no_op_rejected (tests/test_wsdl_parity_gate.py:74-78) and test_silent_data_loss_rejected (tests/test_wsdl_parity_gate.py:116-120) both check only result.exit_code != 0. With xfail(strict=True), that means PR 2 will be "validated" if any change makes the command exit non-zero — not necessarily because the Pattern A / Pattern B guard was added.

Concrete failure mode: if PR 2 adds a new required=True Click option to e.g. ads update for an unrelated reason, ["ads", "update", "--id", "1", "--type", "TEXT_AD"] will fail with Missing option ..., the strict-xfail flips green, and we declare the empty-payload bug fixed without actually having added a guard.

The INTERNAL_VALIDATION pattern you already use (substring match on the expected error string) handles this correctly for Pattern C. Worth applying the same pattern here — e.g. assert the output mentions "no fields" / "empty" / "specify" for Pattern A, and "incompatible with --type" for Pattern B. Otherwise the gate's PR 2 sign-off is partly cosmetic.

Fix this →


2. Footgun: RUNTIME_DEPRECATED_METHODS filter uses CLI group names, but the registry is keyed by API service names

tests/test_wsdl_parity_gate.py:407-412:

relevant = {
    cmd
    for cmd in _mutating_commands()
    if (cmd.split(".", 1)[0], cmd.split(".", 1)[1])
    not in RUNTIME_DEPRECATED_METHODS
}

cmd.split(".", 1)[0] is a CLI group name (e.g. retargeting, dynamicads), but RUNTIME_DEPRECATED_METHODS in direct_cli/wsdl_coverage.py:139-148 is keyed by the API service name (e.g. retargetinglists, dynamictextadtargets). Today the only deprecated entry is ("agencyclients", "add") where CLI=API, and agencyclients.add is in DANGEROUS not WRITE_SANDBOX, so it never reaches this filter — it works by accident.

The moment someone adds a new entry like ("retargetinglists", "add") to RUNTIME_DEPRECATED_METHODS, the filter silently fails to exclude retargeting.add and the coverage test starts demanding a COMMAND_WSDL_MAP entry for a runtime-rejected command. Worth either translating through CLI_TO_API_SERVICE here, or normalizing RUNTIME_DEPRECATED_METHODS to CLI keys.

Fix this →


3. _wsdl_strategy_subtype_names hardcodes the scalar field set

tests/test_wsdl_parity_gate.py:350-358:

scalar_fields = {"AttributionModel", "Name", "CounterIds", "PriorityGoals"}
return [itf["name"] for itf in (strategies_field.get("item_fields") or [])
        if itf["name"] not in scalar_fields]

If Yandex adds a new scalar field to StrategyAddItem (e.g. Budget, CurrencyCode), it will be returned as a strategy subtype name and the PR 2 fix in STRATEGY_TYPES will look wrong. This is a maintenance-burden trap, not a correctness bug — but the gate's whole point is to catch WSDL drift, and this particular check goes blind to the kind of drift it's supposed to detect.

A more durable filter would be the xsd:choice element marker inside StrategyAddItem (since the per-strategy subtype block is a choice, not a sequence of scalars). If that's too much complexity for PR 1, at minimum a # TODO comment so a future reader knows the scalar list must be updated alongside WSDL refreshes.


Nits (non-blocking)

  • next(f for f in schema["fields"] if f["name"] == "Strategies") in _wsdl_strategy_subtype_names raises StopIteration (→ RuntimeError on py3.7+) if the field is missing. Use next(..., None) + an explicit assertion so a WSDL schema rename produces a readable failure.
  • INTERNAL_VALIDATION_PROBES[("creatives", "add", "VideoExtensionCreative")] runs ["creatives", "add"] and expects "Missing option '--video-id'". This works today only because --video-id is the only required option on creatives add. If someone adds another required=True Click option to creatives add later, Click will report that one instead and the probe will break. Adding a second mock argument that satisfies any future required field would make this less fragile.
  • tests/test_wsdl_parity_gate.py:147 registers ("dynamicads", "add"): ("dynamictextadtargets", "add", "Webpages") — correct, but worth noting in CLAUDE.md that the gate's (cli_group, cli_op) → (api_service, wsdl_op, container) triple is where this CLI/API renaming gets recorded for the parity check; the conventions block in your diff documents the map but doesn't call out that CLI-vs-API name divergence is the reason the value tuple isn't redundant.

Verified clean

  • INTERNAL_VALIDATION substrings all match actual UsageError strings in direct_cli/commands/adimages.py:98, bidmodifiers.py:312-315, retargeting.py:96.
  • get_required_item_fields correctly handles the min_occurs default (defaults to 1 if absent, matching XSD semantics).
  • Coverage test correctly excludes *-passport-organization, set-bids, set-auto, and all non-add/update/set ops via the exact-match filter on op in {"add","update","set"}.
  • xfail(strict=True) is the right choice — flipping to passing in PR 2 forces removal of the marker, no silent skip.
  • No new code paths run against the live API; everything goes through cached WSDLs in tests/wsdl_cache/ and CliRunner with --dry-run. No data-loss surface.

Recommendation

Merge-blocking only if you want the PR 2 sign-off to be substantive: tighten #1 (assert on error substrings, not just exit_code). Items #2 and #3 are footguns that won't bite today but will bite later — I'd at least add # TODO comments so the next person finds them before they rot.

  • Read changed files
  • Inspect related modules
  • Look for critical issues
  • Post review
    · branch: worktree-milestone-0.3.9-pr1-wsdl-gate

Strengthen the WSDL parity gate per claude[bot] review:

1. Pattern A/B xfail tests now also assert a guard-reason substring in
   ``result.output``. Previously they only checked ``exit_code != 0``,
   which meant PR 2 could flip strict-xfail green from any unrelated
   ``Missing option '--foo'`` instead of the actual empty-payload or
   per-type-validation guard.

2. ``test_command_wsdl_map_covers_known_mutating_commands`` now routes
   the CLI command through ``CLI_TO_API_SERVICE`` before checking
   ``RUNTIME_DEPRECATED_METHODS``. The previous lookup worked only by
   coincidence (no current deprecated CLI group has a name that differs
   from its API service); adding such an entry would silently break
   the filter.

3. ``_wsdl_strategy_subtype_names`` no longer hardcodes the scalar field
   set. Instead it filters by WSDL type-name pattern: per-strategy
   subtype fields all have a type ending in ``AddItem``, so the gate
   now adapts to any new scalar Yandex adds to ``StrategyAddItem``.
   Also replaced ``next(...)`` with ``next(..., None)`` + explicit
   assertion so a missing ``Strategies`` field gives a readable error
   instead of ``StopIteration``.

Refs #198, PR #205 review.

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

axisrow commented May 20, 2026

Copy link
Copy Markdown
Owner Author

Triage of review nits:

Nit 2 — creatives.add probe fragility: SKIP. The probe only invokes ["creatives", "add"] (no options), and INTERNAL_VALIDATION matches on the substring "Missing option '--video-id'". If a new required=True option is added to creatives add later, Click will report that one instead, the substring won't match, and test_internal_validation_rejects_missing_field[creatives.add/VideoExtensionCreative] will fail loudly — not silently. The gate fails explicitly, which is the intended behavior. Not a footgun.

Nit 3 — CLAUDE.md note that CLI-vs-API rename lives in the tuple: SKIP. The convention block already documents COMMAND_WSDL_MAP as the canonical place to register a new mutating command, and the in-file docstring of the constant explains the triple shape. Adding "and by the way this is where renames go" reads as redundant against the tuple type itself (tuple[str, str, str] — three distinct identifiers). Happy to add it if a future maintainer trips on it, but I'd rather not preempt.

@axisrow

axisrow commented May 20, 2026

Copy link
Copy Markdown
Owner Author

@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production.

@claude

claude Bot commented May 20, 2026

Copy link
Copy Markdown

Claude finished @axisrow's task in 2m 44s —— View job


Review — focused on critical issues (re-run after fix commit 36841b4)

Re-read the diff against origin/main and the fix(test): address PR #205 review feedback commit (36841b4). All three load-bearing items from the previous review have been addressed cleanly; the two triaged nits are reasonable to skip.


Item #1 — xfail substring assertion ✅ fixed

tests/test_wsdl_parity_gate.py:56-108 and :119-161 now carry an expected_error substring per probe:

  • Pattern A probes assert "at least one".lower() in result.output.lower() — verified against the codebase: grep -n "at least one" direct_cli/commands/*.py returns 19 occurrences, so this matches the established wording convention across clients.py, campaigns.py, retargeting.py, bids.py, etc. PR 2 will pick up the convention naturally.
  • Pattern B probe pins on "--image-hash" — any per-type validation error that mentions the offending flag passes; an unrelated Missing option '--foo' will not.

Net effect: strict-xfail can no longer flip green from an unrelated UsageError introduced for a different reason in PR 2. This was the only merge-blocking concern from the prior review and it's resolved.

Item #2RUNTIME_DEPRECATED_METHODS filter ✅ fixed

tests/test_wsdl_parity_gate.py:456-467 adds _is_runtime_deprecated() which routes cli_group through CLI_TO_API_SERVICE before the membership check, with an identity fallback for groups whose name matches the API service. test_command_wsdl_map_covers_known_mutating_commands now calls this helper instead of inlining the raw split. A future entry like ("retargetinglists", "add") in RUNTIME_DEPRECATED_METHODS will now correctly skip the CLI retargeting.add command.

Item #3 — hardcoded scalar field set ✅ fixed

tests/test_wsdl_parity_gate.py:391-414 replaces the hardcoded scalar-field set with a WSDL type-pattern filter: (itf.get("type") or "").endswith("AddItem"). Verified against tests/wsdl_cache/strategies.xml:515-538:

  • Scalar fields use types AttributionModelEnum, string, ArrayOfLong, PriorityGoalsArray — none end in AddItem, correctly excluded.
  • All 16 subtype fields use Strategy*AddItem types — correctly included.

Also replaced next(...) with next(..., None) + explicit assertion ("WSDL drift: strategies.add request schema has no top-level 'Strategies' field. Refresh tests/wsdl_cache/strategies.xml and update the gate.") so a WSDL rename of Strategies gives a readable failure instead of StopIteration/RuntimeError.

Residual risk: if Yandex ever introduces a per-strategy block whose type does not end in AddItem, the gate silently under-reports it. That would also break Yandex's own naming convention — acceptable.


Triaged nits — agree with skip

  • Nit 2 (creatives.add probe fragility): Agree. A new required=True option on creatives add would make Click report the new option's name, the INTERNAL_VALIDATION substring "Missing option '--video-id'" wouldn't match, and test_internal_validation_rejects_missing_field[creatives.add/VideoExtensionCreative] fails loudly. Not a footgun — the gate fails closed.
  • Nit 3 (CLAUDE.md note on CLI↔API rename): Agree. The tuple[str, str, str] shape is self-documenting; the entry ("dynamicads", "add"): ("dynamictextadtargets", "add", "Webpages") already encodes the divergence visibly. No need to preempt.

Verified clean (re-check)

  • No new code paths run against the live API — fetch_wsdl() reads from tests/wsdl_cache/, CliRunner().invoke(... --dry-run) doesn't touch the network. No data-loss surface.
  • get_required_fields / get_required_item_fields (added in direct_cli/wsdl_coverage.py:536-564) are pure XML traversal with sane defaults (min_occurs defaults to 1, matching XSD semantics).
  • xfail(strict=True) keeps PR 2's sign-off honest — flipping to passing forces marker removal, no silent skip.
  • Coverage filter (tests/test_wsdl_parity_gate.py:449-453) keeps exact-match on {"add","update","set"} so set-bids/set-auto/add-passport-organization aren't pulled in.
  • Substring assertions are case-insensitive (.lower() in .lower()), so PR 2 isn't pinned to exact casing.

Recommendation

No further changes required. The fix commit is a substantive response to the prior critical-issue review; the gate's PR 2 sign-off is now substantive rather than cosmetic. Safe to merge.

PR #201 made ``--business-type`` required for ``feeds add``, but the
existing VCR cassette consumed by ``sandbox_feed`` was recorded before
that change. Adding the flag would invalidate the cassette body match,
and re-recording requires live sandbox credentials.

Treat the missing-option error as a known regression and ``pytest.skip``
rather than ``pytest.fail`` so Quality CI is unblocked. The cassette
refresh remains a separate follow-up.

This is the same failure mode that has been red on ``main`` since #201
merged — not introduced by this PR.

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

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

Adds a new system-level test “gate” that checks WSDL ↔ CLI parity for mutating (add/update/set) sandbox-write commands, with a couple of small supporting utilities and documentation updates. This fits into the existing WSDL coverage / contract-testing approach by turning recurring WSDL/CLI drift bug classes into automated, centrally maintained pytest checks.

Changes:

  • Introduces tests/test_wsdl_parity_gate.py with parametrized invariants for empty-payload no-ops, silent flag drops, WSDL required-field enforcement, and strategy enum drift (with targeted xfails for known bugs).
  • Extends direct_cli/wsdl_coverage.py with helpers for extracting required fields from the parsed request schema.
  • Updates integration fixture behavior in tests/conftest.py around feeds add + updates CLAUDE.md with “how to extend the gate” guidance.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
tests/test_wsdl_parity_gate.py New parity-gate test suite spanning all WRITE_SANDBOX mutating commands, plus coverage enforcement that new commands must be mapped.
tests/conftest.py Adjusts sandbox_feed fixture behavior to skip on a known missing-option regression to avoid breaking existing VCR replay.
direct_cli/wsdl_coverage.py Adds helpers to extract minOccurs>=1 top-level and item-level required fields from the request schema.
CLAUDE.md Documents the new parity gate and the extension points (COMMAND_WSDL_MAP, WSDL_FIELD_TO_CLI_OPTION).

Comment on lines +303 to +308
api_service, wsdl_op, container = COMMAND_WSDL_MAP[command_key]
schema = get_operation_request_schema(fetch_wsdl(api_service), wsdl_op)
wsdl_required = get_required_item_fields(schema, container)
if not wsdl_required:
pytest.skip(f"{cli_group}.{cli_op}: WSDL declares no required item fields")

Comment on lines +232 to +235
"SourceType": {"--url", "--file"}, # derived inside the command
"Id": {"--id"},
"Keyword": {"--keyword"},
"ImageData": {"--file", "--image-data"},
Comment on lines +254 to +268
# Fields whose required-ness is enforced inside the command body rather
# than via Click ``required=True``. Each entry is the substring that must
# appear in the CLI's UsageError when the field is missing — the gate
# invokes the command without the relevant flags and asserts the error.
INTERNAL_VALIDATION: dict[tuple[str, str, str], str] = {
(
"adimages",
"add",
"ImageData",
): "Provide exactly one of --image-data or --image-file",
("bidmodifiers", "set", "Id"): "Provide either --id",
("bidmodifiers", "set", "BidModifier"): "Missing option '--value'",
("retargeting", "add", "Rules"): "Provide at least one --rule",
("creatives", "add", "VideoExtensionCreative"): "Missing option '--video-id'",
}
Comment thread tests/conftest.py
Comment on lines +437 to +459
# PR #201 made --business-type required for `feeds add`, but the VCR
# cassette consumed by sandbox_feed was recorded before that. Adding
# the flag would invalidate the cassette body match, and re-recording
# needs live sandbox credentials. Treat the missing-option error as a
# known regression and skip rather than fail until the cassette is
# refreshed.
_FEED_REGRESSION_PATTERNS = (
"Missing option '--business-type'",
)


@pytest.fixture
def sandbox_feed(unique_suffix):
"""Create a feed in sandbox, yield its ID, delete on teardown."""
result = _fixture_invoke(
result = _invoke(
"feeds", "add",
"--name", f"test-feed-{unique_suffix}",
"--url", "https://example.com/feed.xml",
label="feeds add",
)
if result.exit_code != 0:
if _is_sandbox_error(result.output, extra_patterns=_FEED_REGRESSION_PATTERNS):
pytest.skip(f"feeds add fixture skipped: {result.output[:200]}")
pytest.fail(f"feeds add failed (not a sandbox error): {result.output[:500]}")
axisrow added a commit that referenced this pull request May 20, 2026
* fix(cli): close 12 High bugs from #198 audit + drop xfail markers

Per the systemic-audit issue #198, the WSDL parity gate landed in PR
#205 marked 8 commands as xfail-strict pending their fixes. This PR
applies the fixes one by one and drops every xfail / KNOWN_*_BUGS
exemption, so the gate now passes outright (37 passed, 8 skipped).

Patterns and fixes:

* H1, H5, H8, H9, H10 — empty-payload no-op guard:
  - ``ads update``, ``adgroups update``, ``keywords update``: if the
    built payload has only ``Id``, raise UsageError listing the
    available update fields.
  - ``bids set``, ``keywordbids set``: require at least one bid value
    before constructing the request.

* H2 — silent data loss on ``ads update``:
  - Per-type allow-list (TEXT_AD / TEXT_IMAGE_AD / MOBILE_APP_AD)
    rejects incompatible flags up front (e.g. ``--image-hash`` with
    ``--type TEXT_AD``) instead of silently dropping them.

* H4 — ``adgroups add --region-ids`` is now ``required=True``
  (WSDL ``AdGroupAddItem.RegionIds`` minOccurs=1).

* H6 — ``campaigns add --type SMART_CAMPAIGN --counter-id`` is now
  required (WSDL ``SmartCampaignAddItem.CounterId`` minOccurs=1).

* H7 — ``dynamicads add`` no longer requires ``--condition``; the WSDL
  field is minOccurs=0 and the CLI was over-constraining.

* H11 — ``STRATEGY_TYPES`` in ``direct_cli/commands/strategies.py`` is
  rebuilt from ``StrategyAddItem``: dropped 5 names that were not in
  the WSDL (WbMaximumClicksPerBid, WbMaximumConversionRatePerBid,
  AverageCrrPerCampaign, MaxProfitPerFilter, MaxProfitPerCampaign),
  added 5 that were missing (AverageCpcPerCampaign, AverageCpcPerFilter,
  PayForConversionCrr, AverageCpaMultipleGoals,
  PayForConversionMultipleGoals). Membership sets (CPA / CRR / PFC /
  multi-goal) updated accordingly.

* H12 — ``strategies update`` now validates ``--goal-id`` for the
  goal-id strategy family, mirroring ``strategies add``.

* H13 — verified false-positive. WSDL ``ClientUpdateItem`` does NOT
  declare ``ClientId`` minOccurs=1; the login goes through the
  Client-Login header, and ``clients update`` already enforces
  "Provide at least one field to update". No code change here.

The parity gate (``tests/test_wsdl_parity_gate.py``) drops every
``xfail`` marker and the ``KNOWN_REQUIRED_FIELD_BUGS`` exemption set,
so any regression of these patterns will now fail CI on the spot.

Three pre-existing tests updated to match the new validation surface:
- ``test_adgroups_add_case_insensitive_default_type`` now passes
  ``--region-ids`` (now required).
- ``test_campaigns_add_smart_requires_filter_average_cpc`` now passes
  ``--counter-id`` (now required, validated before
  ``--filter-average-cpc``).
- ``test_dynamicads_add_requires_condition`` rewritten as
  ``test_dynamicads_add_without_condition_omits_conditions_field``
  to reflect the new minOccurs=0 behaviour.

Refs #198. Closes PR 2 of milestone 0.3.9.

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

* fix(strategies): align with WSDL on update GoalId and Crr-family

Addresses PR #208 review:

* claude[bot] H12 over-constraint: StrategyAverageCpaBase, StrategyPayForConversionBase,
  StrategyAverageCrrBase, StrategyPayForConversionCrrBase, StrategyAverageCpaPerCampaignBase
  all carry GoalId minOccurs=0, and every Strategy*UpdateItem is an empty extension of its
  *Base. Per strict WSDL parity the update command MUST accept payloads without --goal-id,
  so the issue #198 H12 validation block is removed (with a comment explaining why the
  add-time minOccurs=1 check still stands).
* copilot #1: PayForConversionCrrAddItem schema is Crr + GoalId (no Cpa). Moved
  PayForConversionCrr out of PAY_FOR_CONVERSION_STRATEGY_TYPES into CRR_STRATEGY_TYPES so
  --average-crr now maps to Crr, not the WSDL-unknown Cpa.
* copilot #2: StrategyPayForConversionMultipleGoalsAddItem.GoalId is minOccurs=1, so the
  type is added to GOAL_ID_STRATEGY_TYPES; the add command now rejects payloads without
  --goal-id for that subtype. AverageCpaMultipleGoals stays out — its AddItem has no
  GoalId field at all.

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

* fix(strategies): route --average-cpc to FilterAverageCpc for AverageCpcPerFilter

Addresses PR #208 re-review:

* claude[bot] critical: StrategyAverageCpcPerFilterAddItem has no AverageCpc
  field — its WSDL schema is FilterAverageCpc + WeeklySpendLimit + ... . The
  CLI used to emit {AverageCpc: N} unconditionally, producing a WSDL-invalid
  payload for `--type AverageCpcPerFilter`. Added a FILTER_CPC_STRATEGY_TYPES
  branch mirroring the existing FILTER_CPA_STRATEGY_TYPES handling so
  `--average-cpc` maps to `FilterAverageCpc` for that subtype only.
* claude[bot] observation: AverageCpaMultipleGoals and PayForConversion-
  MultipleGoals AddItems have no Cpa/AverageCpa field at all. Now
  `_build_strategy_fields` raises `UsageError` if `--average-cpa` is passed
  with a multi-goal subtype, instead of silently emitting an unknown key.

Tests:

* test_strategies_add_average_cpc_per_filter_payload_uses_filter_field locks
  the new dispatch.
* test_strategies_add_pay_for_conversion_crr_payload_uses_crr_field confirms
  the earlier commit's CRR-family classification still routes through `Crr`.
* test_strategies_add_multi_goal_rejects_average_cpa exercises the
  multi-goal guard.
* test_strategies_add_pay_for_conversion_multi_goal_requires_goal_id
  exercises the GOAL_ID_STRATEGY_TYPES extension from the prior commit.

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

* fix(strategies): reject empty-subtype no-op on update

PR #208 re-review found one more silent no-op of the same class this PR
closes for ads/adgroups/bids/keywordbids/keywords:

```
direct strategies update --id 42 --type AverageCpa
```

…built `{Id: 42, AverageCpa: {}}` and the live API accepted it as a 200 OK
no-op. A user mistyping a flag name got a successful response with no diff
applied.

Two guards now reject this:

* if `--type X` is present but `_build_strategy_fields(...)` returned `{}`,
  the command errors with "strategies update requires at least one field
  for --type X.";
* if after all flag merging `strategy_data` still contains only `Id` (no
  --name, no --counter-ids, no --priority-goal, no --attribution-model),
  the command errors with "strategies update requires at least one
  updatable field."

Also locked the regression down in the WSDL parity gate: added a
`strategies.update` probe to `EMPTY_PAYLOAD_PROBES` alongside the existing
five so future refactors cannot silently re-open the no-op.

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

---------

Co-authored-by: axisrow <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
axisrow added a commit that referenced this pull request May 21, 2026
… (#217)

* fix(0.3.9): remove feed cassette workaround + Copilot follow-ups

Closes #206.

- tests/conftest.py: drop `_FEED_REGRESSION_PATTERNS` skip workaround;
  sandbox_feed now passes `--business-type RETAIL` (matching what
  `TestWriteFeeds.test_add_update_delete` already does). A cassette
  body mismatch (until refresh) is now reported as an explicit
  `pytest.skip` pointing at the rewrite command, instead of a stale
  `Missing option '--business-type'` skip.
- tests/test_wsdl_parity_gate.py: assert that COMMAND_WSDL_MAP's
  container exists in the WSDL request schema before the no-required-
  fields skip. Misnamed containers can no longer be silently masked
  (Copilot follow-up from PR #205).
- tests/test_wsdl_parity_gate.py: clean `WSDL_FIELD_TO_CLI_OPTION` —
  `SourceType` → `{--url}`, `ImageData` → `{--image-data, --image-file}`.
  Remove the non-existent generic `--file` flag (Copilot follow-up from
  PR #205).
- direct_cli/commands/bidmodifiers.py: hide the rejected legacy
  `--campaign-id`/`--type` options on `bidmodifiers set` via the
  existing `keywords update`-style eager-callback pattern
  (hidden=True, expose_value=False, is_eager=True). `--help` no
  longer advertises the rejected path while the UsageError text is
  preserved for regression coverage (Copilot follow-up from PR #214).
- tests/test_dry_run.py: adapt `test_bidmodifiers_set_id_and_legacy_flags_are_mutex`
  to assert on the canonical "legacy --campaign-id/--type shape is not
  supported" message — legacy flags now fail before the mutex check via
  eager callback semantics.
- CHANGELOG.md: add 0.3.9 (Unreleased) Fixed bullets covering all four
  sub-fixes.

Note: VCR cassettes `TestWriteFeeds.test_add_update_delete.yaml` and
`TestWriteSmartAdTargets.test_add_update_delete.yaml` need response-side
refresh against a valid sandbox login (current response bodies contain
a stale `nonexistent username` error). Until then `TestWriteSmartAdTargets`
skips with an actionable "cassette needs refresh" message instead of
the previous misleading `Missing option '--business-type'` skip.

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

* fix(tests): refresh feed cassettes against live sandbox

Completes issue #206 cassette refresh path.

- tests/conftest.py: _invoke now also passes --login; without it the
  CLI resolved login from the active `direct auth` profile, which
  rarely matches the sandbox token's account. Also invert the
  credentials chain for tests (env vars > profile) per CLAUDE.md, so
  a plain `pytest` never silently hits production via an active
  profile.
- tests/conftest.py: sandbox_smart_adgroup now passes
  --counter-id 12345678 — SmartCampaignAddItem.CounterId is
  WSDL minOccurs=1 (this requirement was previously masked by the
  feed workaround).
- tests/test_integration_write.py: TestWriteFeeds wraps the `feeds
  update` failure path in `_is_sandbox_error` + skip, mirroring the
  add path. The sandbox occasionally returns 8800 on the just-created
  feed; this is a sandbox quirk, not a CLI regression.
- tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml
  and TestWriteSmartAdTargets.test_add_update_delete.yaml: re-recorded
  via `pytest -m integration_write --record-mode=rewrite -k
  "TestWriteFeeds or TestWriteSmartAdTargets"`. Body now contains
  `"BusinessType":"RETAIL"` for `feeds.add`. Both tests skip on
  authentic sandbox limitations instead of the missing-option proxy.
- Removed the transitional "Can't overwrite existing cassette" skip
  branch in `sandbox_feed`: bodies now match.
- CHANGELOG.md: past-tense wording, no PENDING follow-up note.

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

---------

Co-authored-by: axisrow <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
axisrow added a commit that referenced this pull request May 21, 2026
Reword 0.3.10 entry from "fix per-type rejection" to "cover per-type
rejection" to avoid implying a behavioral fix shipped in this release —
the corrected rejection behavior was already shipped in 0.3.9 via the
#198 audit follow-up PRs (#205/#208/#214/#202/#204). This PR only
adds regression coverage.

Addresses Copilot review comment on PR #226.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
axisrow added a commit that referenced this pull request May 21, 2026
…226)

* test: lock down subtype validation invariants from #210 repro matrix

Add 9 new SILENT_LOSS_PROBES in tests/test_wsdl_parity_gate.py covering
campaigns add, adgroups add, ads add, bidmodifiers add and strategies add
incompatible flag/--type combinations from the #210 umbrella repro matrix.
Add 3 non-regression tests in tests/test_dry_run.py for strategies update
field aliases (AverageCpcPerFilter→FilterAverageCpc, PayForConversion→Cpa)
and confirm AverageCpa update without --goal-id stays WSDL-valid.

Refs: Closes #210.

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

* docs(changelog): clarify PR is test-only, not behavioral fix

Reword 0.3.10 entry from "fix per-type rejection" to "cover per-type
rejection" to avoid implying a behavioral fix shipped in this release —
the corrected rejection behavior was already shipped in 0.3.9 via the
#198 audit follow-up PRs (#205/#208/#214/#202/#204). This PR only
adds regression coverage.

Addresses Copilot review comment on PR #226.

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

---------

Co-authored-by: axisrow <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[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