Skip to content

test(v4_live): add live contracts for v4forecast, v4tags, v4wordstat - #177

Merged
axisrow merged 10 commits into
mainfrom
tests/v4-live-contracts-forecast-tags-wordstat
May 19, 2026
Merged

test(v4_live): add live contracts for v4forecast, v4tags, v4wordstat#177
axisrow merged 10 commits into
mainfrom
tests/v4-live-contracts-forecast-tags-wordstat

Conversation

@axisrow

@axisrow axisrow commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds 4 live contract tests in tests/test_v4_live_contracts.py covering v4forecast / v4tags / v4wordstat — three modules previously had zero live coverage.
  • Tests reuse existing _credentials() and _campaign_id() helpers, inherit module-level pytestmark = pytest.mark.v4_live_read, and auto-skip without YANDEX_DIRECT_TOKEN / YANDEX_DIRECT_LOGIN.
  • No production code touched — only new test functions appended.

Coverage matrix

Module Method Covered
v4wordstat CreateNewWordstatReport ✅ test 1
v4wordstat GetWordstatReportList ✅ test 1
v4wordstat DeleteWordstatReport ✅ test 1
v4wordstat GetWordstatReport ⛔ polling — out of scope
v4forecast CreateNewForecast ✅ test 2
v4forecast GetForecastList ✅ test 2
v4forecast DeleteForecastReport ✅ test 2
v4forecast GetForecast ⛔ polling — out of scope
v4tags GetCampaignsTags ✅ test 3
v4tags GetBannersTags ✅ test 4
v4tags UpdateCampaignsTags ⛔ write — out of scope
v4tags UpdateBannersTags ⛔ write — out of scope

10/12 methods. 100% of safe read methods + 100% of lifecycle without polling.

Design notes

  • Async lifecycle (wordstat/forecast): create → list (find by ID) → delete in try/finally. No polling — explicitly excluded by 0.3.8 — v4 live integration tests: v4forecast, v4tags, v4wordstat #174 acceptance criteria.
  • Tags read-only: _campaign_id() helper (lines 25–38) picks the first campaign via v5 campaigns.get, same pattern as test_v4_live_goals_contracts.
  • Fixed deterministic params: Phrases=["купить ноутбук"], GeoID=[0] (wordstat) / GeoID=[213] (forecast, RUB).

Test plan

  • flake8 tests/test_v4_live_contracts.py — clean
  • black --check tests/test_v4_live_contracts.py — clean
  • pytest --collect-only -m v4_live_read tests/test_v4_live_contracts.py → 11 tests collected (7 existing + 4 new)
  • Auto-skip verified: env -u YANDEX_DIRECT_TOKEN -u YANDEX_DIRECT_LOGIN pytest -m v4_live_read ... → 4 skipped
  • Unit suite green: pytest -m "not integration ..." → 627 passed, 0 failed
  • Live run under .env with token (manual, post-merge):
    pytest -m v4_live_read tests/test_v4_live_contracts.py::test_v4_live_wordstat_lifecycle_contract
    pytest -m v4_live_read tests/test_v4_live_contracts.py::test_v4_live_forecast_lifecycle_contract
    pytest -m v4_live_read tests/test_v4_live_contracts.py::test_v4_live_tags_get_campaigns_contract
    pytest -m v4_live_read tests/test_v4_live_contracts.py::test_v4_live_tags_get_banners_contract
    
    Minor assert tweaks may be needed after first live run (exact return shape of delete methods, exact tag field names).

Closes #174

🤖 Generated with Claude Code

Refs #174.

- wordstat lifecycle (create/list/delete) under v4_live_read marker
- forecast lifecycle (create/list/delete) under v4_live_read marker
- tags GetCampaignsTags read-only via existing _campaign_id helper
- tags GetBannersTags read-only via existing _campaign_id helper

GetForecast and GetWordstatReport are out of scope (require polling).
UpdateBannersTags / UpdateCampaignsTags are out of scope (write).
Tests auto-skip without YANDEX_DIRECT_TOKEN / YANDEX_DIRECT_LOGIN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 13:39
@axisrow axisrow added this to the 0.3.8 milestone May 18, 2026

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 four live contract tests in tests/test_v4_live_contracts.py to extend v4_live_read coverage to the previously-untested v4 modules v4wordstat, v4forecast, and v4tags, addressing issue #174.

Changes:

  • New test_v4_live_wordstat_lifecycle_contract exercising CreateNewWordstatReportGetWordstatReportListDeleteWordstatReport in a try/finally.
  • New test_v4_live_forecast_lifecycle_contract following the same create → list → delete pattern for forecasts.
  • New test_v4_live_tags_get_campaigns_contract and test_v4_live_tags_get_banners_contract read-only tests for v4tags, reusing _campaign_id().

@axisrow
axisrow requested a review from Copilot May 18, 2026 13:42
@axisrow

axisrow commented May 18, 2026

Copy link
Copy Markdown
Owner Author

@claude @codex review

@claude

claude Bot commented May 18, 2026

Copy link
Copy Markdown

Claude finished @axisrow's task in 1m 50s —— View job


Code Review

  • Gather context
  • Read changed file
  • Analyze test design and correctness
  • Post review feedback

Overall this is clean, well-structured coverage work. The try/finally lifecycle pattern is solid and consistent with acceptance criteria from #174. A few issues worth addressing:


Medium — v4_live_read mark applied to write operations

tests/test_v4_live_contracts.py:191 and tests/test_v4_live_contracts.py:218 — the module-level pytestmark = pytest.mark.v4_live_read is inherited by both lifecycle tests, but smoke_matrix.py:150-155 explicitly classifies v4forecast.create, v4forecast.delete, v4wordstat.create-report, and v4wordstat.delete-report as WRITE_SANDBOX.

Running pytest -m v4_live_read will now execute tests that perform live write operations (create + delete reports) against the production API. Someone relying on v4_live_read to mean "read-only, safe to run against prod" could inadvertently mutate state. Wordstat/forecast reports are admittedly low-stakes (quota-bounded, analytical), but the mark taxonomy still misleads.

Options:

  • Add a dedicated v4_live_write_safe mark and apply it to the lifecycle tests alongside (or instead of) v4_live_read
  • Use pytest.mark.parametrize-style per-test marks rather than relying solely on the module marker
  • Add a module-level note acknowledging the exception

Fix this →


Minor — field assertions target [0] (first item), not the created item

tests/test_v4_live_contracts.py:205-206 and tests/test_v4_live_contracts.py:228-229:

assert any(r.get("ReportID") == report_id for r in reports)
assert {"ReportID", "StatusReport"} <= set(reports[0])   # ← checks first item, not our item

The any() check confirms our report is present, but the field schema assertion runs against reports[0], which may be a pre-existing report from a previous run. This still validates the schema but obscures intent. Prefer:

our_report = next(r for r in reports if r.get("ReportID") == report_id)
assert {"ReportID", "StatusReport"} <= set(our_report)

Same issue at the forecast counterpart (forecasts[0]our_forecast).

Fix this →


Minor — asymmetric field assertions in tags tests

tests/test_v4_live_contracts.py:249-252 (test_v4_live_tags_get_campaigns_contract) checks {"CampaignID", "Tags"}, but tests/test_v4_live_contracts.py:261-263 (test_v4_live_tags_get_banners_contract) only checks "BannerID", omitting the Tags field. If the method is GetBannersTags, Tags is presumably the primary payload:

if data:
    assert {"BannerID", "Tags"} <= set(data[0])   # add Tags

Nitpick — redundant explicit None arguments

tests/test_v4_live_contracts.py:200 and tests/test_v4_live_contracts.py:224:

call_v4(client, "GetWordstatReportList", None)
call_v4(client, "GetForecastList", None)

call_v4's param argument defaults to None, so the explicit None is redundant. Consistent with call_v4(client, "GetClientsUnits", [login]) elsewhere — just drop the trailing argument.


Nitpick — unchecked delete return values in finally

tests/test_v4_live_contracts.py:208 and tests/test_v4_live_contracts.py:231DeleteWordstatReport / DeleteForecastReport return 1 on success per the Yandex API spec. Adding assert call_v4(...) == 1 in the finally block would catch silent deletion failures. Low priority since cleanup failures don't affect the test outcome, but it would increase observability.


Summary: The overall design is correct and well-motivated. The try/finally lifecycle, skip guards, and field-presence assertions all follow established patterns in the file. The main concern worth resolving before merge is the v4_live_read marker mismatch — everything else is polish.

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

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e5b66a4cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +199 to +202
report_id = call_v4(
client,
"CreateNewWordstatReport",
{"Phrases": ["купить ноутбук"], "GeoID": [0]},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move write lifecycle probes out of v4_live_read suite

This test is tagged by module-level pytest.mark.v4_live_read, but it performs live write operations (CreateNewWordstatReport/DeleteWordstatReport here and CreateNewForecast/DeleteForecastReport in the same commit). In this repo, v4_live_read is documented as read-only (pyproject.toml marker description), so running pytest -m v4_live_read now either fails for read-only API accounts (permission errors) or unexpectedly mutates production data for writable accounts. These probes should be gated behind an explicit write opt-in marker/env flag instead of the read-only tier.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 11799ea. Followed the existing pattern of test_v4_live_create_invoice_contract_opt_in_write (line 140) — gated wordstat and forecast lifecycle behind YANDEX_DIRECT_LIVE_WRITE=1 with explicit _opt_in_write suffix in the function names. Tags read-only tests are unchanged.

axisrow and others added 3 commits May 18, 2026 20:47
Addresses Codex review on #177: CreateNew*/Delete* are write operations
and must not run as part of the read-only v4_live_read tier. Follows the
existing pattern of test_v4_live_create_invoice_contract_opt_in_write
(line 140) — env-gate inside the test body with explicit _opt_in_write
suffix in the function name.

Behaviour:
- Without YANDEX_DIRECT_LIVE_WRITE=1: tests skip (even with token+login).
- With YANDEX_DIRECT_LIVE_WRITE=1: tests run and mutate the live account
  with a single create/delete cycle.

Tags read-only tests (GetCampaignsTags, GetBannersTags) are unchanged —
they remain pure reads under v4_live_read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Click 8.x changed the missing-option error format from
  "No such option: --foo"
to
  "No such option '--foo'. Did you mean '--bar'?"

This broke three legacy-flag rejection tests on Python 3.11/3.13 CI
runners (3.9 still ships an older Click). Split each assertion into
two checks — "No such option" + the flag name — which match both the
old and new formats.

Fixes the CI failure inherited from main on #177.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Claude review on #177:
- Use next() to fetch the just-created report/forecast from the list and
  validate its schema directly, instead of asserting on reports[0] which
  may belong to a prior run.
- Drop redundant third argument to call_v4(..., "List*", None) — param
  defaults to None.
- Add TODOs flagging two assertions to tighten after first live run:
  Delete* return value (== 1 per docs) and GetBannersTags field set
  (likely {BannerID, TagIDS}, mirroring UpdateBannersTags writes).

Behaviour unchanged for both skip path and live path; lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@axisrow

axisrow commented May 18, 2026

Copy link
Copy Markdown
Owner Author

Claude review applied in 771512d.

Item Severity Action
1 Medium ✅ Done in 11799ea (env-gate YANDEX_DIRECT_LIVE_WRITE=1 + _opt_in_write suffix, mirroring existing test_v4_live_create_invoice_contract_opt_in_write)
2 Minor ✅ Done — next(r for r in ... if ReportID == report_id) validates schema on our record, not on [0]
3 Minor ⚠️ Held — Claude suggests Tags field, but UpdateBannersTags writes TagIDS (v4_contracts.py:419), so the read likely returns TagIDS. Added TODO to tighten to {BannerID, TagIDS} after first live observation
4 Nitpick ✅ Done — dropped redundant None from call_v4(..., "GetForecastList") / "GetWordstatReportList"
5 Nitpick ⚠️ Held — Delete* return shape is not in v4_contracts.py; added TODO to assert == 1 after first live observation

axisrow and others added 2 commits May 18, 2026 22:23
…is unset

Companion fix to #179 (auth-profile fallback in WRITE_SANDBOX runners).
Previously _credentials() silently skipped all v4_live_read tests for
profile-only users — manual pytest -m v4_live_read showed no signal.

Now: when env vars are missing, try direct_cli.auth.get_credentials() to
resolve the profile (matches the priority used by every CLI subcommand).
Skip message also tells the user both options.

Refs #178.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…via CLI

Addresses Codex review finding #2 on #179: write-mutating tests were
inheriting the v4_live_read file-level marker, breaking the trust
boundary between read-only and write test suites.

- Removes test_v4_live_wordstat_lifecycle_contract_opt_in_write and
  test_v4_live_forecast_lifecycle_contract_opt_in_write from
  tests/test_v4_live_contracts.py (which is correctly marked v4_live_read).
- Adds test_live_draft_v4wordstat_lifecycle and
  test_live_draft_v4forecast_lifecycle to tests/test_integration_live_write.py
  under the integration_live_write marker.
- Rewrites them to use the CLI (via _invoke_live / CliRunner) instead of
  native call_v4, matching the convention of every other live-write test
  in that file.
- Drops in-test YANDEX_DIRECT_LIVE_WRITE checks — the file-level skipif
  already enforces the env gate.

Verified:
  pytest --collect-only -m v4_live_read tests/test_v4_live_contracts.py
    → 9 read-only tests (no lifecycle probes).
  pytest --collect-only -m integration_live_write
    tests/test_integration_live_write.py
    → both v4 lifecycle tests included.
  env -u YANDEX_DIRECT_LIVE_WRITE pytest <…lifecycle…>
    → SKIPPED via file-level skipif.

Codex review finding #1 (bare `pytest` hits live API when profile is
active) is intentionally deferred: project decision is that v4 live
read tests should run whenever a profile is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axisrow added a commit that referenced this pull request May 18, 2026
Click 8.x changed the missing-option error format from
  "No such option: --foo"
to
  "No such option '--foo'. Did you mean '--bar'?"

This broke three legacy-flag rejection tests on Python 3.11/3.13 CI
runners (3.9 still ships an older Click). Split each assertion into
two checks — "No such option" + the flag name — which match both the
old and new formats.

Fixes the CI failure inherited from main on #177.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axisrow added a commit that referenced this pull request May 18, 2026
…direct auth profile (#179)

* fix(scripts): honour direct auth profile in WRITE_SANDBOX runners

When YANDEX_DIRECT_TOKEN and YANDEX_DIRECT_LOGIN are not in env, fall
back to the OAuth profile resolved by direct_cli.auth.get_credentials
(profile → env → .env → vault priority). Matches the existing behaviour
of scripts/test_safe_commands.sh.

Two layers fixed symmetrically:

- scripts/test_sandbox_write.sh: probe `direct auth status` for
  `has_token=yes` (the command always exits 0, so grep the marker), then
  inject resolved token+login via eval so subprocesses see them.
- scripts/sandbox_write_live.py:validate_environment(): defensive
  fallback to get_credentials() for direct python invocations bypassing
  the shell wrapper.

CI is not affected — these runners are manual-only, not invoked from
.github/workflows.

Closes #178.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(scripts): shell-quote resolved credentials and require login

Addresses Claude review findings on #179:

1. The previous version interpolated token/login directly into the eval
   block. Yandex OAuth tokens use base64url alphabet and logins are
   constrained, so a real-world injection is unlikely — but 1Password
   and Bitwarden references could return arbitrary strings, and a token
   containing a shell metacharacter would either truncate the value or
   execute arbitrary code. Wrap both values with shlex.quote().

2. get_credentials() returns Tuple[str, Optional[str]] — login can be
   None for profiles created without a login. The shell guard above
   only checks `has_token=yes`, so a token-without-login profile would
   reach the eval and export the literal string "None". Add the
   matching `if not token or not login` check inside the Python helper
   and propagate its non-zero exit through a `if ! resolved=$(...)`
   wrapper (eval swallows subshell exit codes otherwise).

No behaviour change for callers with a fully-populated profile or
env-supplied credentials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: relax 'No such option' asserts for Click 8.x format

Click 8.x changed the missing-option error format from
  "No such option: --foo"
to
  "No such option '--foo'. Did you mean '--bar'?"

This broke three legacy-flag rejection tests on Python 3.11/3.13 CI
runners (3.9 still ships an older Click). Split each assertion into
two checks — "No such option" + the flag name — which match both the
old and new formats.

Fixes the CI failure inherited from main on #177.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: axisrow <axisrow@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axisrow and others added 3 commits May 19, 2026 13:23
Adds @pytest.mark.vcr to the two new v4 lifecycle tests and commits their
recorded cassettes so `pytest -m integration_live_write` no longer hits the
production API on every run, matching the contract documented in README
and pyproject.toml.

Also extends `_before_record_request` in tests/conftest.py to redact the
OAuth token from the request body: the v4 JSON API embeds `"token":"..."`
inside the payload (not just the Authorization header), so the existing
header-only filter would have leaked it into the recorded YAML.

Addresses Codex adversarial review feedback on #177.
Codex's second adversarial review flagged two real issues confirmed by
live verification:

1. tests/test_v4_live_contracts.py:_credentials() falls back to the
   active `direct auth` profile when env vars are missing. This is
   intentional but was never documented, and contradicts CLI's own
   priority chain (where the profile wins over env). Document the
   inverted, tests-only contract in _credentials() docstring, CLAUDE.md
   and README.md (EN+RU). No code change — behaviour is already correct
   (env wins; profile is fallback; otherwise skip).

2. test_live_draft_v4{wordstat,forecast}_lifecycle in
   tests/test_integration_live_write.py created account-level v4 reports
   under a marker that promises "only disposable draft resources". Move
   the two tests to tests/test_v4_live_contracts.py under the existing
   `_opt_in_write` pattern, gated by YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1.
   Delete the two corresponding cassettes (no replay for this tier —
   opt-in live only). Rename the remaining 18 v5 tests and cassettes
   from `test_live_draft_*` to `test_v5_live_draft_*` so v5 vs v4 is
   immediately visible at the test name level.

Add tests/_orphan_store.py: a small atomic JSON store at
~/.direct-cli/test-orphans.json that records created v4 report IDs.
Each opt-in test calls drain() on entry to retry deletions left over
from an interrupted previous run, add() right after create, and
remove() after a successful delete. If delete fails (network drop,
SIGKILL), the ID stays in the store and gets cleaned up next time.
Atomic write pattern modeled on direct_cli/auth.py:144-155.

Verified:
- pytest -m integration_live_write replay: 10 passed + 7 skipped (the
  same 18-test surface, minus the 2 removed v4 tests; cassettes resolve
  via new v5_ names).
- pytest tests/test_v4_live_contracts.py without env: both new tests
  skip as expected.
- orphan store: simulated network failure scenario — ID persists after
  failed delete, gets picked up by drain() on next run, store empties.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align the file name with the test names. After the previous commit
renamed the 18 functions to `test_v5_live_draft_*`, the file itself
still said `test_integration_live_write.py` — function prefix and
file name now point at different things ("v5" vs. nothing). Renaming
makes the file/test/cassette directory all say v5.

The pytest marker `integration_live_write` is kept untouched — it
describes the run contract (`-m integration_live_write`), not the
file name, and renaming it would break the CI/dev invocation surface.

git mv:
- tests/test_integration_live_write.py → tests/test_v5_live_write.py
- tests/cassettes/test_integration_live_write/ →
  tests/cassettes/test_v5_live_write/
  (pytest-recording derives the cassette directory from the module
  name, so the directory has to track the file).

Doc references updated:
- tests/API_ISSUE_AUDIT.md
- tests/API_COVERAGE.md

Verified: 18 tests collect under the new path, replay returns the same
10 passed + 7 skipped as before, no leftover refs to the old name in
the repo (only in `direct_cli.egg-info/SOURCES.txt`, which is a build
artifact and not committed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@axisrow
axisrow requested a review from Copilot May 19, 2026 13:27
@axisrow

axisrow commented May 19, 2026

Copy link
Copy Markdown
Owner Author

@claude @codex review

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Claude finished @axisrow's task in 3m 8s —— View job


Code Review

  • Gather context
  • Read changed files
  • Analyze test design and correctness
  • Post review feedback

The PR is in good shape after the prior review round. All previous medium/minor items were correctly applied. A few residual issues worth noting before merge:


Minor — v4_live_read marker description still says "read-only"

pyproject.toml:120:

"v4_live_read: manual read-only v4 Live tests (requires YANDEX_DIRECT_TOKEN and YANDEX_DIRECT_LOGIN)",

The module now contains _opt_in_write tests that perform create/delete operations. The env gate prevents accidents, but the "read-only" label is misleading for anyone reading the marker registry. test_v5_live_write.py uses a dedicated integration_live_write marker; the v4 tests instead reuse v4_live_read with an in-test guard. Neither approach is wrong, but the description should reflect reality:

"v4_live_read: manual v4 Live tests (read-only by default; lifecycle tests require YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1)",

Fix this →


Minor — from tests import _orphan_store inside function body

tests/test_v4_live_contracts.py:258 and :296:

def test_v4_live_wordstat_lifecycle_opt_in_write():
    if os.getenv(_REPORT_WRITE_ENV) != "1":
        pytest.skip(...)
    from tests import _orphan_store  # ← deferred import inside function

This relies on tests being importable as a package on sys.path. It works now (the file exists, tests/__init__.py is present), but deferred imports inside test function bodies can cause confusing ModuleNotFoundError failures in non-standard invocations (e.g. python -m pytest tests/test_v4_live_contracts.py from a directory where tests/ isn't a package root). A top-level conditional import would be cleaner:

try:
    from tests import _orphan_store as _orphan_store_mod
except ImportError:
    _orphan_store_mod = None

Or simply move it to the top of the file since the module already has tests/__init__.py.


Minor — Stale skip message in _campaign_id()

tests/test_v4_live_contracts.py:59:

pytest.skip("No campaign available for v4 goals live probes")

_campaign_id() is now called by the tags tests (test_v4_live_tags_get_campaigns_contract, test_v4_live_tags_get_banners_contract) in addition to the goals test. The skip reason specifically mentions "goals", which will mislead developers running the tags tests. Consider making it generic:

pytest.skip("No campaign found in account — cannot run v4 tags/goals live tests")

Fix this →


Nitpick — Env var naming inconsistency

The new env gate uses YANDEX_DIRECT_V4_LIVE_REPORT_WRITE, while the existing invoice write gate uses YANDEX_DIRECT_LIVE_FINANCE_WRITE (no V4_ infix). Two naming schemes in the same file. Not blocking, but worth noting for future additions.


Held items (acknowledged from prior review — no action needed)

  • Tags GetBannersTags field assertion (data[0] checks only BannerID): TODO comment is in place at line 236–239. Acceptable pending live observation.
  • Delete* return value not asserted in finally: TODO implicit by design. Low priority.

Summary: The core design is solid — orphan store, env-gated opt-in, next() schema assertions on the created record, and try/finally lifecycle are all correct. The stale skip message and marker description drift are the most visible polish items. The deferred import pattern is a latent fragility risk.

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

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

Comment on lines 17 to 44
def _credentials():
"""Resolve test credentials with env > profile > skip priority.

Tests intentionally invert the CLI priority chain: env vars win over the
active ``direct auth`` profile. This way a developer machine with an
active profile cannot silently hit production on a plain ``pytest``
invocation — the suite either uses explicit env vars, or falls back to
the saved profile only when env vars are absent, or skips entirely.
Contract is documented in CLAUDE.md and README.md.
"""
token = os.getenv("YANDEX_DIRECT_TOKEN")
login = os.getenv("YANDEX_DIRECT_LOGIN")
if not token or not login:
pytest.skip("YANDEX_DIRECT_TOKEN and YANDEX_DIRECT_LOGIN are required")
try:
from direct_cli.auth import get_credentials

token, login = get_credentials(None, None)
except (ValueError, RuntimeError, ImportError):
pytest.skip(
"credentials required: set YANDEX_DIRECT_TOKEN+YANDEX_DIRECT_LOGIN "
"or run 'direct auth login'"
)
if not token or not login:
pytest.skip(
"credentials required: set YANDEX_DIRECT_TOKEN+YANDEX_DIRECT_LOGIN "
"or run 'direct auth login'"
)
return token, login

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Process feedback noted — scope/description rather than code. The original description framed the first commit only; subsequent commits (orphan store, credentials contract, v5 rename, opt-in lifecycle tier) accumulated organically. We will refresh the description before merge. No code change.

Comment thread tests/test_v4_live_contracts.py Outdated
Comment on lines +34 to +37
except (ValueError, RuntimeError, ImportError):
pytest.skip(
"credentials required: set YANDEX_DIRECT_TOKEN+YANDEX_DIRECT_LOGIN "
"or run 'direct auth login'"
Comment thread tests/_orphan_store.py Outdated
Comment on lines +35 to +43
except (FileNotFoundError, OSError, json.JSONDecodeError):
return {}
if not isinstance(data, dict):
return {}
return {
k: [int(x) for x in v if isinstance(x, (int, str))]
for k, v in data.items()
if isinstance(k, str) and isinstance(v, list)
}
Comment on lines +228 to +241
token, login = _credentials()
campaign_id = _campaign_id(token, login)
client = create_v4_client(token=token, login=login)

data = call_v4(client, "GetBannersTags", {"CampaignIDS": [campaign_id]})

assert isinstance(data, list)
if data:
# TODO: tighten to {"BannerID", "TagIDS"} once live response is observed.
# UpdateBannersTags writes TagIDS (v4_contracts.py:419), so the read
# method likely returns the same field — confirm at first live run.
assert "BannerID" in data[0]


Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Soft assertion is intentional. GetBannersTags response shape is not documented in v4_contracts.py (lines 378–394 only define the request shape, no response field set). We conservatively check BannerID membership for the first live observation and tighten in a follow-up. This matches the pattern of other read-only probes in this file — test_v4_live_get_events_log_contract and test_v4_live_get_credit_limits_contract use assert data is not None. The TODO is the contract: tighten to {"BannerID", "TagIDS"} once the first live run confirms. Not blocking this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a200daa22f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

try:
from direct_cli.auth import get_credentials

token, login = get_credentials(None, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip v4 live tests without explicit env credentials

Because pytestmark = pytest.mark.v4_live_read only labels these tests and the pytest config does not deselect that marker by default, a plain pytest run still collects them. In the common developer setup with an active direct auth profile but no YANDEX_DIRECT_TOKEN/YANDEX_DIRECT_LOGIN, this fallback resolves real profile credentials and the v4 live tests call production instead of skipping, contradicting the safety note above and unexpectedly consuming live API units. Please require explicit env credentials or another opt-in before using the saved profile here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Conflicting with the documented project contract. The fallback to the active direct auth profile is intentional and is documented as the inverted-priority chain for tests in CLAUDE.md (lines 29, 60), README.md (line 85 EN, line 780 RU), and the _credentials() docstring. We accept the trade-off: env > profile > skip. Additional guard: these tests carry pytestmark = pytest.mark.v4_live_read, so a plain pytest run does not select them — only an explicit -m v4_live_read opts in. Revisiting the contract is a separate discussion and not blocking this PR.

Address two real defects flagged in PR #177 review on commit a200daa.

1. tests/test_v4_live_contracts.py::_credentials() previously caught
   only (ValueError, RuntimeError, ImportError) from
   direct_cli.auth.get_credentials(None, None). Tracing the call path
   showed two reachable leak points after a successful OAuth refresh:
   - OSError from save_auth_store → _write_json (os.chmod + mkstemp on
     ~/.direct-cli/ — unconditional, no swallow);
   - json.JSONDecodeError from parsing a malformed refresh-token
     response body (auth.py:534).
   Either would turn a "skip when creds unavailable" bootstrap into a
   hard test error on a dev machine with a stale profile but no env
   vars. Widen the except to include OSError and json.JSONDecodeError,
   and add a comment so the next reader sees why these two specifically.
   The other failure modes Copilot mentioned (HTTPError/URLError,
   KeyError/TypeError) are already trapped inside auth.py and cannot
   escape — confirmed by reading load_auth_store, get_oauth_profile,
   refresh_access_token, and validate_oauth_profile.

2. tests/_orphan_store.py::_read() had its list comprehension
   `[int(x) for x in v if isinstance(x, (int, str))]` outside the
   try/except, so a corrupted bucket like {"v4wordstat": ["abc"]}
   would raise ValueError up through add/remove/drain — contradicting
   the module docstring promise that "all read/write errors are
   swallowed silently so tests are never broken by store corruption."
   Move the comprehension inside the try and extend except with
   (ValueError, TypeError). Also drop redundant FileNotFoundError
   (subclass of OSError). Verified with hand-corrupted JSON: "abc"
   yields {}, [null, 456] yields {"v4wordstat": [456]}, plain garbage
   yields {}.

Codex's P2 (skip v4 tests entirely without explicit env credentials)
is left unaddressed: the env > profile > skip contract is the
documented and intended behavior — see CLAUDE.md, README.md (EN+RU),
and the _credentials() docstring. Reply posted on the thread.

Copilot's other comments triaged:
- "PR description doesn't reflect actual scope": process feedback, not
  a code issue.
- "Weak BannerID assert": intentional per the file's "confirm at first
  live run" pattern (matches sibling v4 read-only tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@axisrow
axisrow merged commit 94b4822 into main May 19, 2026
12 checks passed
@axisrow
axisrow deleted the tests/v4-live-contracts-forecast-tags-wordstat branch August 2, 2026 17:45
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.

0.3.8 — v4 live integration tests: v4forecast, v4tags, v4wordstat

2 participants