test: automated API coverage check via WSDL introspection - #34
Conversation
Add WSDL-based coverage tests that detect missing CLI commands and methods by comparing against Yandex Direct API v5 service definitions. - wsdl_coverage.py: WSDL fetching, parsing, CLI↔API service/method mapping - test_api_coverage.py: Phase 1 (method coverage) and Phase 2 (service discovery) tests - wsdl_cache/: 27 cached WSDL XML files for offline CI - pyproject.toml: add api_coverage pytest marker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds automated WSDL-driven test coverage checks to compare the CLI’s registered commands/subcommands against Yandex Direct API v5 service definitions, using a committed WSDL cache to keep CI offline.
Changes:
- Introduces
direct_cli/wsdl_coverage.pyutilities to fetch/cache WSDL XML and map CLI subcommands to API operations. - Adds
tests/test_api_coverage.py(pytestapi_coveragemarker) to detect missing API methods per service and newly introduced API services. - Commits cached WSDL XML files under
tests/wsdl_cache/and registers theapi_coveragemarker inpyproject.toml.
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| direct_cli/wsdl_coverage.py | WSDL fetch/parse + CLI↔API service/method mapping utilities used by coverage tests |
| tests/test_api_coverage.py | Coverage tests for missing/extra methods and missing/new services using cached WSDLs |
| pyproject.toml | Registers api_coverage pytest marker |
| tests/wsdl_cache/adextensions.xml | Cached WSDL for offline coverage checks (adextensions) |
| tests/wsdl_cache/adgroups.xml | Cached WSDL for offline coverage checks (adgroups) |
| tests/wsdl_cache/adimages.xml | Cached WSDL for offline coverage checks (adimages) |
| tests/wsdl_cache/ads.xml | Cached WSDL for offline coverage checks (ads) |
| tests/wsdl_cache/advideos.xml | Cached WSDL for offline coverage checks (advideos) |
| tests/wsdl_cache/agencyclients.xml | Cached WSDL for offline coverage checks (agencyclients) |
| tests/wsdl_cache/audiencetargets.xml | Cached WSDL for offline coverage checks (audiencetargets) |
| tests/wsdl_cache/bidmodifiers.xml | Cached WSDL for offline coverage checks (bidmodifiers) |
| tests/wsdl_cache/bids.xml | Cached WSDL for offline coverage checks (bids) |
| tests/wsdl_cache/businesses.xml | Cached WSDL for offline coverage checks (businesses) |
| tests/wsdl_cache/campaigns.xml | Cached WSDL for offline coverage checks (campaigns) |
| tests/wsdl_cache/changes.xml | Cached WSDL for offline coverage checks (changes) |
| tests/wsdl_cache/clients.xml | Cached WSDL for offline coverage checks (clients) |
| tests/wsdl_cache/creatives.xml | Cached WSDL for offline coverage checks (creatives) |
| tests/wsdl_cache/dictionaries.xml | Cached WSDL for offline coverage checks (dictionaries) |
| tests/wsdl_cache/dynamictextadtargets.xml | Cached WSDL for offline coverage checks (dynamictextadtargets) |
| tests/wsdl_cache/feeds.xml | Cached WSDL for offline coverage checks (feeds) |
| tests/wsdl_cache/keywordbids.xml | Cached WSDL for offline coverage checks (keywordbids) |
| tests/wsdl_cache/keywords.xml | Cached WSDL for offline coverage checks (keywords) |
| tests/wsdl_cache/keywordsresearch.xml | Cached WSDL for offline coverage checks (keywordsresearch) |
| tests/wsdl_cache/leads.xml | Cached WSDL for offline coverage checks (leads) |
| tests/wsdl_cache/negativekeywordsharedsets.xml | Cached WSDL for offline coverage checks (negativekeywordsharedsets) |
| tests/wsdl_cache/retargetinglists.xml | Cached WSDL for offline coverage checks (retargetinglists) |
| tests/wsdl_cache/sitelinks.xml | Cached WSDL for offline coverage checks (sitelinks) |
| tests/wsdl_cache/smartadtargets.xml | Cached WSDL for offline coverage checks (smartadtargets) |
| tests/wsdl_cache/turbopages.xml | Cached WSDL for offline coverage checks (turbopages) |
| tests/wsdl_cache/vcards.xml | Cached WSDL for offline coverage checks (vcards) |
| import requests | ||
|
|
||
| url = WSDL_BASE_URL.format(service=service_name) | ||
| resp = requests.get(url, timeout=30) | ||
| resp.raise_for_status() | ||
|
|
||
| xml_text = resp.text | ||
|
|
There was a problem hiding this comment.
fetch_wsdl imports and uses requests, but requests is not listed in project dependencies. This will raise ModuleNotFoundError whenever the cache is missing or use_cache=False (e.g., running refresh_all_caches()), and it won’t be caught by the offline tests.
Consider either switching to the stdlib (urllib.request) for the HTTP GET, or adding requests as an explicit runtime dependency and ensuring it’s available in CI environments that refresh the cache.
| import requests | |
| url = WSDL_BASE_URL.format(service=service_name) | |
| resp = requests.get(url, timeout=30) | |
| resp.raise_for_status() | |
| xml_text = resp.text | |
| from urllib.request import urlopen | |
| url = WSDL_BASE_URL.format(service=service_name) | |
| with urlopen(url, timeout=30) as resp: | |
| xml_text = resp.read().decode("utf-8") |
| import xml.etree.ElementTree as ET | ||
| from pathlib import Path | ||
|
|
||
| WSDL_BASE_URL = "https://api.direct.yandex.com/v5/{service}?wsdl" | ||
| CACHE_DIR = Path(__file__).resolve().parent.parent / "tests" / "wsdl_cache" | ||
|
|
There was a problem hiding this comment.
CACHE_DIR points into a tests/wsdl_cache directory relative to the installed package. In an installed environment this path likely won’t exist (tests aren’t included in the package) and may also be unwritable (site-packages), causing unexpected network fetches and/or write failures.
Consider making the cache directory configurable (parameter/env var) and defaulting to a user-writable cache location (e.g., XDG cache dir), or moving cached WSDLs into package data and reading them via importlib.resources when use_cache=True.
| import xml.etree.ElementTree as ET | |
| from pathlib import Path | |
| WSDL_BASE_URL = "https://api.direct.yandex.com/v5/{service}?wsdl" | |
| CACHE_DIR = Path(__file__).resolve().parent.parent / "tests" / "wsdl_cache" | |
| import os | |
| import xml.etree.ElementTree as ET | |
| from pathlib import Path | |
| WSDL_BASE_URL = "https://api.direct.yandex.com/v5/{service}?wsdl" | |
| def _get_cache_dir(): | |
| """Return a writable cache directory for downloaded WSDL files.""" | |
| cache_dir = os.environ.get("DIRECT_CLI_WSDL_CACHE_DIR") | |
| if cache_dir: | |
| return Path(cache_dir).expanduser() | |
| xdg_cache_home = os.environ.get("XDG_CACHE_HOME") | |
| if xdg_cache_home: | |
| return Path(xdg_cache_home).expanduser() / "direct_cli" / "wsdl_cache" | |
| return Path.home() / ".cache" / "direct_cli" / "wsdl_cache" | |
| CACHE_DIR = _get_cache_dir() |
| # API methods that exist in WSDL but the CLI does not implement. | ||
| # When one is fixed, remove it from this set and the test will still pass. | ||
| KNOWN_MISSING_METHODS = { | ||
| ("agencyclients", "addPassportOrganization"), | ||
| ("agencyclients", "addPassportOrganizationMember"), |
There was a problem hiding this comment.
The suppression sets (KNOWN_MISSING_SERVICES/KNOWN_MISSING_METHODS/KNOWN_EXTRA_CLI_METHODS/ALLOWED_EXTRA_METHODS) can mask future regressions if they become stale. If a missing method/service is later implemented but not removed from the relevant KNOWN_* set, a subsequent regression that removes it again would be ignored.
Consider adding assertions that each KNOWN_MISSING_* entry is currently missing (and each KNOWN_EXTRA_* entry is currently extra), so the lists must be updated when coverage changes.
…VICES Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Item (Demographics/Retargeting/Regional/SerpLayout/IncomeGrade)
…w project pattern
Реализует оставшиеся 6 пунктов из follow-up плана issue #32 для "100% covered supported API surface". Инфраструктура (direct_cli/wsdl_coverage.py): - CLI_ALIAS_GROUPS для dynamictargets/smarttargets/negativekeywords - NON_WSDL_SERVICE_POLICIES — явная политика для reports и др. non-SOAP surface - INTENTIONAL_EXTRA_METHODS с reasons (заменяет KNOWN_MISSING_METHODS-стиль) - расширены METHOD_NAME_OVERRIDES - registry-driven schema: get_api_coverage_policy, get_operation_request_schema Команды (22 файла): добавлен --dry-run во все lifecycle-команды (delete/archive/unarchive/suspend/resume/moderate). Новые команды: agencyclients add-passport-organization/add-passport-organization-member/update, audiencetargets set-bids, dynamicads set-bids. reports: выделена чистая build_report_request() функция. Тесты (tests/test_api_coverage.py, tests/test_dry_run.py): - 70+ PAYLOAD_CASES с реальными dry-run прогонами и проверкой формы запроса - DRY_RUN_PAYLOAD_EXCLUSIONS с обоснованиями - test_non_wsdl_services_have_explicit_coverage_policy - test_alias_groups_resolve_to_real_cli_groups - test_api_coverage_report_script_matches_strict_parity_contract - end-to-end contract тесты reports (build_report_request + client.reports().post) Скрипты и CI: - scripts/build_api_coverage_report.py — JSON-отчёт strict parity / summary - scripts/check_wsdl_drift.py — сравнение cached WSDL с live API - .github/workflows/api-coverage.yml — fast suite + report artifact + weekly drift check Integration test: убран dynamicads.update (операция отсутствует в WSDL dynamictextadtargets, это часть приведения CLI к strict WSDL parity). Docs: README секция "API Coverage And Drift Monitoring" с таблицей четырёх surfaces и определением "100% coverage". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@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 finished @axisrow's task in 4m 51s —— View job PR Review — Critical IssuesTasks:
Bug 1 —
|
| File | Line | Pattern |
|---|---|---|
bids.py |
144 | int(max_bid * 1000000) in set-auto |
audiencetargets.py |
93 | int(bid * 1000000) in add |
audiencetargets.py |
137 | int(context_bid * 1000000) in set-bids |
smartadtargets.py |
274 | int(average_cpc * 1000000) in set-bids |
smartadtargets.py |
277 | int(average_cpa * 1000000) in set-bids |
dynamicads.py |
203, 205 | int(bid * 1000000) in set-bids |
keywordbids.py |
84, 85 | int(search_bid * 1000000) in set |
Fix: use round(bid * 1_000_000) (or equivalently int(round(bid * 1_000_000))). The same bug exists in the pre-existing bids.set and bids.get commands but those predate this PR.
Bug 3 — keywordsresearch sends wrong method name casing Fix this →
The cached WSDL (tests/wsdl_cache/keywordsresearch.xml) defines operations as:
hasSearchVolume
deduplicate
(lowercase camelCase, consistent with all other Yandex Direct SOAP operations)
But keywordsresearch.py sends:
# line 31
body = {"method": "HasSearchVolume", ...}
# line 58
body = {"method": "Deduplicate", ...}These are PascalCase, not camelCase. The API likely rejects or ignores them. Every other command in the codebase uses lowercase-initial names (get, add, setBids, setAuto, etc.).
The WSDL coverage test test_service_method_coverage does not catch this because get_cli_methods_for_service builds the method set from METHOD_NAME_OVERRIDES (which correctly maps "has-volume" → "hasSearchVolume"), but never validates the string actually present in the command's request body. This bug predates this PR but the new coverage tests are silently failing to detect it.
Observation — _collect_complex_type_fields has unbounded recursion
wsdl_coverage.py:224–253 recursively follows XSD type inheritance with no cycle guard. Valid schemas can't be circular, but a corrupted or adversarially-crafted WSDL cache file would cause infinite recursion and crash the test process. Low severity given these are committed files, but worth noting.
scripts/check_wsdl_drift.py и scripts/refresh_wsdl_cache.py вызывают direct_cli.wsdl_coverage.fetch_wsdl(use_cache=False), который делает lazy `import requests`. Пакет приходит транзитивно через tapi-yandex-direct, но явная декларация защищает от будущего перехода upstream на другой HTTP-клиент (например httpx). Addresses feedback from PR #34 review (claude[bot]). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Триаж ревьюBug 1 — Поэтому CI Bug 2 — Проверка по диапазону 0.01–99.99 показывает, что Bug 3 — Observation — unbounded recursion в |
|
@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 finished @axisrow's task in 7m 16s —— View job PR Review — Critical IssuesTasks:
Previous review items — triaged correctlyAll three bugs from the prior review were handled appropriately. The one remaining open item is Bug 3 (PascalCase New Finding 1 — Schema validator silently skips nested type validation for imported XSD namespaces [Medium severity]
complex_types = {
ctype.get("name"): ctype for ctype in schema.findall("xsd:complexType", ns)
}When Result: This isn't a production bug in the CLI commands themselves — the commands are correct. But the New Finding 2 —
|
Делает ограничения `get_operation_request_schema` и `_assert_body_matches_wsdl` явными, а не скрытыми: 1. Типы из xsd:import не резолвятся — item_fields пустые для cross-namespace полей. Nested validation работает только для локально-определённых inline типов (agencyclients Notification, Settings, Grants проходят с пустыми dict-ами без ошибки). 2. xsd:complexContent/xsd:extension у request element теряет поля базового типа. Латентный риск для get* операций, которых сейчас нет в PAYLOAD_CASES. Addresses review Findings 1 & 2 on PR #34 (claude[bot]). Не меняет runtime, production-код корректен (проверено вручную против WSDL). Сами команды валидируются на CLI dry-run уровне. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Триаж ревью #2Finding 1 (Medium) — imported XSD types не валидируются — принято, задокументировано (389df1b). Finding 2 (Low, latent) — xsd:extension теряет поля базы — задокументировано (389df1b). Finding 3 — stale Observation — принято к сведению, никаких действий. |
|
@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 finished @axisrow's task in 6m 19s —— View job PR Review — Round 3Tasks:
Previously flagged items — all resolvedAll bugs and findings from rounds 1 and 2 have been correctly addressed or documented. No action needed on those. New Finding —
|
Summary
direct_cli/wsdl_coverage.py: WSDL fetching, parsing, CLI↔API service/method mapping with cached XML files for offline CItests/test_api_coverage.py: Phase 1 (method coverage per service) and Phase 2 (new service discovery) teststests/wsdl_cache/api_coveragepytest marker added topyproject.tomlDetected gaps (tracked in
KNOWN_*constants):advideosTest plan
pytest tests/test_api_coverage.py -v— 2/2 passed (offline, from cache)pytest -m api_coverage -v— marker filtering workspytest— 120 passed, no regressionsblack --check/flake8— cleanKNOWN_MISSING_METHODS→ test fails with clear messageCANONICAL_API_SERVICES→test_no_missing_servicesfails🤖 Generated with Claude Code