Skip to content

test: live-testing matrix for untested CLI commands via disposable draft pattern #59

Description

@axisrow

Проблема

CLI содержит 29 модулей команд, но реальное покрытие тестами неравномерное. Часть команд:

  • не покрыта ни read-, ни write-тестами (leads, turbopages, businesses, advideos, agencyclients);
  • покрыта только VCR-кассетами с sandbox-ошибками, но никогда не проверялась против live API (категории A/B из test: roadmap for write integration beyond current Yandex sandbox limits #28);
  • требует особого testing pattern (draft campaign tree) и не может быть проверена тем же способом, что standalone-ресурсы;
  • принципиально не автоматизируется (ads moderate, suspend/resume на активных кампаниях).

Перед закрытием milestone 0.3.0 и заявлением о 100% coverage нужен структурный план: что покрываем через live-тесты на disposable draft, что требует других подходов, а что честно оставляем manual-only.

Что такое "черновик" (draft) в Яндекс.Директ

Из WSDL (tests/wsdl_cache/campaigns.xml, tests/wsdl_cache/ads.xml):

  • Campaign Status: DRAFT, MODERATION, ACCEPTED, REJECTED
  • Campaign State: OFF, ON, SUSPENDED, CONVERTED
  • Ad Status: DRAFT, MODERATION, PREACCEPTED, ACCEPTED, REJECTED

Draft campaign (как в PR #57):

  • создаётся через campaigns add с --start-date в будущем;
  • Status=DRAFT, State=OFF;
  • не обслуживает трафик, не тратит деньги, не попадает в аукцион;
  • можно удалить без последствий (campaigns delete).

Ограничения черновика:

  • пока Status=DRAFTsuspend/resume меняют State внутри уже активной кампании, поэтому на DRAFT вернут валидную ошибку или no-op (это смоук-тест payload, не lifecycle-тест);
  • archive/unarchive логически применимы к любой кампании, но архивация draft бессмысленна;
  • ads moderate существует только для объявлений и отправляет их в модерацию — односторонняя операция, автоматизировать нельзя;
  • установка bids на черновом keyword безопасна (не вступает в силу до активации), это хороший смоук-тест.

Disposable draft pattern (из tests/test_integration_live_write.py)

create draft campaign (Status=DRAFT, State=OFF, start-date in future)
  → create nested resource inside draft (adgroup/ad/keyword/...)
  → verify CLI round-trip
  → delete nested (optional, cascade by campaign delete)
  → delete campaign (cascade cleanup)
finally:
  ensure campaign is deleted even on test failure

Обязательные правила для всех live-тестов (уже соблюдаются в PR #57 для root-фикстуры):

  • требовать YANDEX_DIRECT_LIVE_WRITE=1;
  • принимать только свои собственные созданные ID;
  • cleanup в finally;
  • никогда не вызывать moderate;
  • никогда не трогать существующие кампании пользователя.

Категория 1: МОЖНО через disposable draft

1.1 Standalone assets (не требуют draft campaign вообще)

Команда Pattern Риск
sitelinks add/get/delete standalone sitelink set низкий
adimages add/get/delete standalone asset низкий
vcards add/get/delete standalone низкий (уже есть VCR, нет live)
adextensions add/get/delete standalone низкий (уже есть VCR, нет live)
retargeting add/update/delete standalone retargeting list низкий (уже есть VCR, нет live)
negativekeywordsharedsets add/update/delete standalone низкий (уже есть VCR, нет live)
feeds add/update/delete standalone feed низкий (уже есть VCR, нет live)
advideos add/get требует статичный --url или --video-data средний
creatives add --video-id/get chain advideo → creative средний

1.2 Nested inside draft campaign

Требуют fixture chain: live_draft_campaign → live_draft_adgroup → live_draft_keyword/ad.

Команда Что нужно создать перед Можно на draft?
adgroups add/update/delete draft campaign да
ads add/update/delete (TEXT_AD) draft + draft adgroup да
keywords add/update/delete draft + draft adgroup да
keywords archive/unarchive draft + adgroup + keyword да
bids set draft + adgroup + keyword да (не вступает в силу до активации)
bids set-auto draft + adgroup + keyword да
keywordbids set draft + adgroup + keyword да
keywordbids set-auto draft + adgroup + keyword да
bidmodifiers add/set/delete draft campaign да
audiencetargets add/delete draft + draft adgroup + retargeting list да
dynamicads add/update/delete draft DYNAMIC_TEXT_CAMPAIGN + DYNAMIC_TEXT_AD_GROUP да (категория B из #28)
smartadtargets add/update/delete draft SMART_CAMPAIGN + SMART_AD_GROUP да (категория B из #28)
smartadtargets set-bids то же + smartadtarget да
dynamicads set-bids то же + dynamicad да
audiencetargets set-bids то же + audience target да
campaigns archive/unarchive draft campaign да (API разрешает)

1.3 Smoke-тесты на draft: suspend/resume и аналоги

На draft suspend/resume меняют State — ожидаем либо валидную ошибку "cannot change state in DRAFT", либо no-op. Оба исхода подтверждают что CLI правильно собирает payload.

  • keywords suspend/resume
  • audiencetargets suspend/resume
  • dynamicads suspend/resume
  • smartadtargets suspend/resume
  • ads suspend/resume/archive/unarchive на draft ad

Это не lifecycle-покрытие, а регрессионный guard CLI-payload. Заявлять о полном покрытии suspend/resume flow нельзя — нужна отдельная ручная проверка.


Категория 2: НЕЛЬЗЯ через draft

2.1 Односторонние операции (нельзя откатить)

Команда Почему нельзя на draft Альтернатива
ads moderate Отправляет в модерацию → Status=MODERATION → может уйти в ACCEPTED и начать показы. Без удаления объявления отката нет. Ручная проверка на тестовой кампании, которую потом удалить. Никогда не автоматизировать.

2.2 Требуют активной кампании (не draft)

suspend/resume на объектах со статусом ACCEPTED меняют live traffic. На draft возвращают ошибку (это в категории 1.3). Полноценный lifecycle — только ручная проверка.

Команда Проблема Подход
campaigns suspend on ACCEPTED меняет live traffic manual
campaigns resume on SUSPENDED то же manual
ads suspend on ACCEPTED меняет показы manual
ads resume on SUSPENDED то же manual
ads archive on ACCEPTED меняет доступность manual
ads unarchive on archived то же manual

2.3 Тратят деньги на существующих объектах

Правило: никогда не передавать в live-тест ID существующего keyword/campaign/ad пользователя. На disposable draft keyword все set-bids безопасны (категория 1.2).

Команды с риском при применении к чужим ID:

  • bids set --keyword-id <existing>
  • keywordbids set --keyword-id <existing>
  • bidmodifiers set --modifier-id <existing>
  • dynamicads set-bids --target-id <existing>
  • audiencetargets set-bids --target-id <existing>
  • smartadtargets set-bids --target-id <existing>

2.4 Требуют агентский контекст

agencyclients * работает только для агентских аккаунтов (без агентства — 403).

Команда Риск Подход
agencyclients get безопасно (read-only) add TestReadOnlyAgencyClients с graceful skip если 403
agencyclients add создаёт нового клиента под агентством manual
agencyclients update меняет контакты клиента manual
agencyclients delete удаляет клиента manual
agencyclients add-passport-organization связывает паспортную организацию, двусторонняя операция manual
agencyclients add-passport-organization-member --notification отправляет email внешнему пользователю manual, особо опасно

2.5 Отсутствуют в CLI (блокирует #54)

Команда Статус
strategies add/archive/get/unarchive/update команд в CLI нет — сначала реализация, потом тесты
dynamicfeedadtargets add/delete/get/resume/setBids/suspend то же

Рекомендованный порядок работы (по фазам)

Фаза 1 — read-only добор (нулевой риск, можно прямо сейчас)

Добавить в tests/test_integration.py:

  • TestReadOnlyLeadsleads get с graceful skip если нет campaigns;
  • TestReadOnlyTurbopagesturbopages get;
  • TestReadOnlyBusinessesbusinesses get;
  • TestReadOnlyAdVideosadvideos get;
  • TestReadOnlyAgencyClientsagencyclients get с graceful skip если 403.

Закрывает гэп "команда написана, но никогда не вызывалась".

Фаза 2 — standalone draft assets (низкий риск)

Расширить tests/test_integration_live_write.py:

Фаза 3 — nested inside draft campaign (категория A из #28)

Базовая fixture chain: live_draft_campaign → live_draft_adgroup → live_draft_ad/keyword.

Классы:

  • TestLiveWriteAdGroupsDraft
  • TestLiveWriteAdsDraft (add/update/delete; не moderate)
  • TestLiveWriteKeywordsDraft
  • TestLiveWriteBidsDraft
  • TestLiveWriteKeywordBidsDraft
  • TestLiveWriteAudienceTargetsDraft

Фаза 4 — нестандартные типы кампаний (категория B из #28)

  • TestLiveWriteDynamicAdsDraft через DYNAMIC_TEXT_CAMPAIGN
  • TestLiveWriteSmartAdTargetsDraft через SMART_CAMPAIGN

Фаза 5 — suspend/resume smoke-тесты на draft

Покрыть ads/keywords/audiencetargets/dynamicads/smartadtargets suspend/resume на draft-объектах. Регрессионный guard CLI-payload.

Фаза 6 — CLI gap из #54 (новые команды)

Реализовать strategies и dynamicfeedadtargets в direct_cli/commands/, затем применить Фазы 2–5 к новым командам.

Фаза 7 — manual-only, не автоматизировать

Создать tests/MANUAL_COVERAGE.md:

  • ads moderate — manual;
  • campaigns/ads suspend/resume/archive/unarchive на существующих объектах — manual;
  • agencyclients add/update/delete + passport-org operations — manual;
  • bidmodifiers/bids/keywordbids/*_set_bids на существующих объектах — запрещено.

Честный документированный gap.


Файлы по фазам

Фаза Файлы
1 tests/test_integration.py (+5 классов), tests/cassettes/test_integration/
2 tests/test_integration_live_write.py (+4 класса) + fixtures + cassettes
3 tests/test_integration_live_write.py (+6 классов) + chain fixtures
4 tests/test_integration_live_write.py (+2 класса)
5 tests/test_integration_live_write.py (+5 smoke classes)
6 direct_cli/commands/strategies.py, direct_cli/commands/dynamicfeedadtargets.py, direct_cli/cli.py, unit + integration tests
7 tests/MANUAL_COVERAGE.md (новый)

Риски

  • Деньги: draft кампании не тратят бюджет, но ошибка (случайный existing ID) — деньги пользователя. Все fixtures принимают только созданные в своём run ID.
  • Rate limits: много кассет в rewrite-режиме могут исчерпать лимиты. Записывать порциями (--record-mode=rewrite -k <selected>).
  • Кассеты содержат токены: после записи обязательный grep audit на YANDEX_DIRECT_TOKEN и YANDEX_DIRECT_LOGIN. Шаблон в PR test: add guarded live-write draft tier and expand coverage audit #57.
  • Cleanup failure: если finally не сработал — остаётся draft campaign. Периодический ручной sweep через campaigns get --limit 100 + удаление всех с префиксом direct-cli-live-test-*.

Открытые вопросы

  1. Scope первого PR: одним большим PR от Фазы 1 до Фазы 5 или дробить? Рекомендация: дробить — каждая фаза самодостаточна.
  2. Conftest для live-fixtures: отдельный tests/live_conftest.py или inline в test_integration_live_write.py? Рекомендация: inline до > 6 тестов, потом вынести.
  3. Агентский аккаунт: есть ли тестовый агентский аккаунт для Фазы 1 пункта TestReadOnlyAgencyClients? Без него — всегда skip.

Связанные


🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions