Skip to content

fix(changes): mutex ID filters + FieldNames enum validation (#228)#229

Merged
axisrow merged 1 commit into
mainfrom
worktree-fix-228-changes-mutex-ids
May 21, 2026
Merged

fix(changes): mutex ID filters + FieldNames enum validation (#228)#229
axisrow merged 1 commit into
mainfrom
worktree-fix-228-changes-mutex-ids

Conversation

@axisrow

@axisrow axisrow commented May 21, 2026

Copy link
Copy Markdown
Owner

Summary

Брал #228 в работу, провёл триаж против кода и официальной документации Yandex Direct: Changes.check. Большая часть исходных пунктов оказалась про MCP-плагин (передал в axisrow/yandex-direct-mcp-plugin#115) либо была опровергнута кодом (parse_datetime уже нормализует timestamp к ISO-8601-with-Z; никакого MAX_BATCH_SIZE = 10 в CLI нет). Реальный CLI-скоуп — два пункта:

  • Bug Add 1Password auth and simplify build config #2 (FIX): в direct changes check была только --campaign-ids. По WSDL и доке API Changes.check принимает три взаимоисключающих ID-фильтра — CampaignIds / AdGroupIds / AdIds.
  • Bug feat: add Bitwarden CLI integration #3 partial (FIX): --fields не валидировался — тайпо вроде CmapignIds уходило в API и возвращалось мутной серверной ошибкой.

What this PR does

  • Заменяет --campaign-ids required=True тремя mutually-exclusive флагами: --campaign-ids / --ad-group-ids / --ad-ids. Проверка «exactly one of» через click.UsageError, до try/except обёртки — пользователь получает корректный exit code 2 и Usage hint.
  • Валидирует --fields против WSDL-энума CheckFieldEnum = {CampaignIds, AdGroupIds, AdIds, CampaignsStat}. Если значения нет в энуме — UsageError с подсказкой допустимых значений. Дефолт (все четыре) остаётся для UX.
  • Edge cases пойманные адверсариальным ревью: --fields , (давал FieldNames=[] в нарушение minOccurs=1), --campaign-ids , (раньше падал с exit code 1 через parse_ids's ValueErrorAbort) — оба теперь UsageError.
  • Чинит существующую фикстуру в tests/test_low_coverage_payloads.py::test_changes_check_builds_canonical_payload: использовала FieldNames=[\"CampaignId\", \"ChangesIn\"] — не валидные значения CheckFieldEnum по WSDL (это response-поля). Заменено на корректные [\"CampaignIds\", \"CampaignsStat\"].

Out of scope

Tests

12 новых unit-тестов в tests/test_changes.py (CliRunner + patched create_client):

  • mutex: missing-all, two-at-once, all-three-at-once
  • ID-маршрутизация: каждый из трёх флагов → корректное WSDL-поле в body
  • FieldNames: default (все 4 значения), валидный passthrough, unknown enum, mixed valid+invalid
  • edge: --fields , пустой список, --campaign-ids , ValueError из parse_ids → UsageError

Verification

pytest -m "not integration" -q   # 901 passed, 44 skipped
black --check direct_cli tests   # clean on touched files
flake8 direct_cli tests          # clean on touched files

Test plan

  • CI зелёный (quality + test-and-report 3.9/3.11/3.13 + api coverage)
  • Линтеры clean на изменённых файлах
  • --help для direct changes check показывает все три ID-флага и список допустимых FieldNames

Closes #228

🤖 Generated with Claude Code

Yandex Direct Changes.check accepts three mutually-exclusive ID
filters — CampaignIds (up to 3000), AdGroupIds (up to 10 000), AdIds
(up to 50 000). The CLI previously only exposed --campaign-ids (with
required=True), making it impossible to ask the API «did ad 17722
change after X?» without re-fetching the parent campaign. Likewise
--fields accepted any string and forwarded typos to the API as
opaque server-side errors.

This change reshapes `direct changes check`:

* Three options --campaign-ids / --ad-group-ids / --ad-ids; exactly
  one is required. Mutex enforced via click.UsageError BEFORE the
  try/except wrapper so users get a real Usage hint and exit code 2.
* --fields validated against the WSDL CheckFieldEnum
  {CampaignIds, AdGroupIds, AdIds, CampaignsStat}; default behaviour
  (all four) is unchanged when --fields is omitted.
* Empty / comma-only inputs that would otherwise produce empty arrays
  (FieldNames=[] violates WSDL minOccurs=1) or a confusing Abort exit
  code are caught as UsageErrors.

tests/test_changes.py (new) — 12 unit tests via CliRunner with
patched create_client: missing-all / two-at-once / three-at-once
mutex, valid each ID flag (assert correct WSDL field name in body),
default FieldNames coverage, valid passthrough, unknown enum value,
mixed valid+invalid, comma-only --fields, comma-only --campaign-ids.

tests/test_low_coverage_payloads.py — existing
test_changes_check_builds_canonical_payload used
FieldNames=["CampaignId", "ChangesIn"], which are NOT in the
CheckFieldEnum per WSDL (they're response-side fields). Replaced
with the valid pair ["CampaignIds", "CampaignsStat"]. The old
fixture would fail under the new enum gate — and rightly so.

Scope deliberately narrow: numeric size limits (3000/10000/50000),
timestamp normalisation (already handled by parse_datetime), and the
broader UsageError→Abort wrapper refactor (#227) are out of scope.
The MAX_BATCH_SIZE=10 / SKILL.md / timestamp claims in #228 belong
to the MCP plugin and were redirected to
axisrow/yandex-direct-mcp-plugin#115.

Verification:
  pytest -m "not integration" -q   # 901 passed, 44 skipped
  black --check direct_cli tests   # clean on touched files
  flake8 direct_cli tests          # clean on touched files

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Copilot AI review requested due to automatic review settings May 21, 2026 14:02
@axisrow

axisrow commented May 21, 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 21, 2026

Copy link
Copy Markdown

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


Review — no critical issues found ✅

Scope per request: bugs, security, logic errors, data-loss risk, performance. Style / naming / nits skipped intentionally.

  • Read changed files
  • Diff against origin/main
  • Verify mutex logic, FieldNames validation, edge cases
  • Cross-check WSDL fixture
  • Post review

What I checked

1. Mutex enforcement (direct_cli/commands/changes.py:59-70)
The "exactly one of" check happens before the try/except Exception wrapper, so users get a clean UsageError (exit 2 with Usage hint) instead of Abort (exit 1 + colored stderr). Counter-style sources_used correctly handles 0, 2, and 3 simultaneous flags. ✅

2. FieldNames enum (changes.py:17, :72-85)
_CHECK_FIELD_NAMES = {"CampaignIds", "AdGroupIds", "AdIds", "CampaignsStat"} is byte-identical to the WSDL CheckFieldEnum enumeration at tests/wsdl_cache/changes.xml:67-74. Validation runs against this enum before the API call, which is exactly the behavior the PR description promises. ✅

3. Order of validation
mutex → fields → ID parsing → API call. This order is correct: it surfaces the most actionable user-facing error first (which flag combo to pass), then field typos, then parse errors. ✅

4. Edge cases

  • --fields ,f.strip() filter yields [] → explicit UsageError("empty list...") instead of WSDL minOccurs=1 violation server-side. ✅
  • --campaign-ids ,parse_ids raises ValueError("Invalid ID: ''. ..."), caught at changes.py:97 and re-raised as UsageError (exit 2). ✅
  • --fields "CampaignIds, ,CampaignsStat" (whitespace token between commas) → stripped + filtered → passes correctly.
  • Case-sensitive comparison against enum — typos like campaignids rejected. Matches API behavior; correct. ✅

5. Fixture correction (tests/test_low_coverage_payloads.py:66-78)
Old fixture used FieldNames=["CampaignId", "ChangesIn"] — neither is a valid CheckFieldEnum member; those are response-payload field names. New fixture ["CampaignIds", "CampaignsStat"] is WSDL-correct. This change was load-bearing — without it, the new validation would have made that test fail. The fix is right. ✅

6. Integration test (tests/test_integration.py:609-623)
Still uses --campaign-ids only — passes mutex (exactly one filter). No regression. ✅

7. Backward compatibility
The only previously-supported invocation was --campaign-ids <ids>. Still works identically. Two new flags are additive. No breaking change for existing callers. ✅

Things that are not bugs but worth noting

  • field_names = get_default_fields("changes") returns a live reference to COMMON_FIELDS["changes"], not a copy. If tapi_yandex_direct ever mutated params["FieldNames"] during serialization, it would corrupt the module-level default for subsequent calls. This is pre-existing behavior in direct_cli/utils.py:598 and affects every command using get_default_fields, not just this PR. Out of scope for this fix, but worth a follow-up if anyone audits utils.py.
  • The new mutex tests assert result.exit_code != 0 rather than == 2. The PR description explicitly claims "exit code 2" is the contract being established (vs. legacy exit 1). Tests would pass even if Click silently changed to exit 1. Tightening to == 2 would lock in the intent — but this is a test-strictness suggestion, not a production risk.

Summary

Clean fix. The WSDL-derived enum is correctly extracted, the mutex matches the documented Yandex Direct contract, and the order of UsageError checks (mutex before try/except) gives the right exit code. The corrected fixture in test_low_coverage_payloads.py is necessary and accurate.

No blocking issues. Safe to merge.

--- · worktree-fix-228-changes-mutex-ids

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

This PR fixes and tightens the direct changes check CLI command to better match the Yandex Direct Changes.check contract by supporting all mutually-exclusive ID filters and validating FieldNames before sending the request to the API.

Changes:

  • Replace the single required --campaign-ids filter with mutually-exclusive --campaign-ids / --ad-group-ids / --ad-ids and enforce “exactly one of”.
  • Validate --fields against the WSDL enum (CampaignIds, AdGroupIds, AdIds, CampaignsStat) and reject typos / empty lists early.
  • Add focused unit tests for mutex routing + FieldNames validation, and update an existing canonical payload fixture to use valid enum values.

Reviewed changes

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

File Description
direct_cli/commands/changes.py Adds mutex ID filters and pre-flight FieldNames validation for changes check, with improved error reporting for invalid ID inputs.
tests/test_changes.py New unit tests covering mutex enforcement, ID routing, and --fields validation edge cases.
tests/test_low_coverage_payloads.py Fixes the canonical payload fixture to use valid CheckFieldEnum values.
Comments suppressed due to low confidence (2)

tests/test_changes.py:47

  • Same as above: this test intends to assert Click "usage" semantics for mutually-exclusive flags, but only checks exit_code != 0. Asserting exit_code == 2 would ensure the command keeps raising click.UsageError (and not getting wrapped into click.Abort()), matching the PR description about correct exit codes.
def test_check_rejects_two_id_filters_together():
    """Mutex enforcement: passing two filters is a UsageError."""
    with patch("direct_cli.commands.changes.create_client") as create_client:
        result = _invoke("--campaign-ids", "1", "--ad-group-ids", "2")

    assert result.exit_code != 0
    assert "mutually exclusive" in result.output
    create_client.assert_not_called()

tests/test_changes.py:57

  • Same as above: asserting exit_code == 2 here would more directly validate that the "all three flags" path is a click.UsageError (usage exit code) rather than an Abort/runtime failure.
def test_check_rejects_all_three_id_filters_together():
    """Mutex enforcement: even three at once is rejected with the same wording."""
    with patch("direct_cli.commands.changes.create_client") as create_client:
        result = _invoke("--campaign-ids", "1", "--ad-group-ids", "2", "--ad-ids", "3")

    assert result.exit_code != 0
    assert "mutually exclusive" in result.output
    create_client.assert_not_called()

"""Check for changes"""


_CHECK_FIELD_NAMES = frozenset({"CampaignIds", "AdGroupIds", "AdIds", "CampaignsStat"})
Comment on lines 109 to 113
params = {
"CampaignIds": parse_ids(campaign_ids),
id_field: id_value,
"Timestamp": parse_datetime(timestamp),
"FieldNames": field_names,
}
Comment thread tests/test_changes.py
Comment on lines +27 to +37
def test_check_rejects_missing_all_id_filters():
"""Mutex enforcement: at least one of three ID flags is required."""
with patch("direct_cli.commands.changes.create_client") as create_client:
result = _invoke()

assert result.exit_code != 0
assert "Provide exactly one of" in result.output
assert "--campaign-ids" in result.output
assert "--ad-group-ids" in result.output
assert "--ad-ids" in result.output
create_client.assert_not_called()
@axisrow
axisrow merged commit 0e9af29 into main May 21, 2026
10 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

2 participants