Skip to content

fix(scripts): test_sandbox_write.sh and sandbox_write_live.py honour direct auth profile#179

Merged
axisrow merged 3 commits into
mainfrom
fix/sandbox-write-script-auth-profile
May 18, 2026
Merged

fix(scripts): test_sandbox_write.sh and sandbox_write_live.py honour direct auth profile#179
axisrow merged 3 commits into
mainfrom
fix/sandbox-write-script-auth-profile

Conversation

@axisrow

@axisrow axisrow commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

WRITE_SANDBOX runners now fall back to the direct auth login profile when YANDEX_DIRECT_TOKEN/YANDEX_DIRECT_LOGIN aren't in env. Matches the existing behaviour of scripts/test_safe_commands.sh.

Closes #178.

Problem

direct auth login
direct auth status   # has_token=yes
unset YANDEX_DIRECT_TOKEN YANDEX_DIRECT_LOGIN
bash scripts/test_sandbox_write.sh
# → ERROR: YANDEX_DIRECT_TOKEN and YANDEX_DIRECT_LOGIN are required.

Same population can't run v4 live tests from #177 — they silently skip. The companion fix for tests/test_v4_live_contracts.py::_credentials() will go into #177 directly to avoid cross-PR conflict.

Changes

scripts/test_sandbox_write.sh

  • Fallback block when env is empty: probes direct auth status for the literal has_token=yes marker (the command always exits 0, so a non-zero exit can't be the gate).
  • Resolves token+login via inline Python call to direct_cli.auth.get_credentials(None, None) and eval-exports them so the Python child and its direct --sandbox … subprocesses inherit them.

scripts/sandbox_write_live.py:validate_environment()

  • Defensive fallback to get_credentials() for direct Python invocations that bypass the shell wrapper.
  • Mutates os.environ so spawned direct --sandbox … subprocesses see the credentials.

CI is not affected — these runners aren't called from any .github/workflows.

Test plan

Verified locally on macOS with active OAuth profile and no env vars:

  • Scenario 1 — profile only: env -u YANDEX_DIRECT_TOKEN -u YANDEX_DIRECT_LOGIN bash scripts/test_sandbox_write.sh --help → resolves through profile, Python runner exec'd ✓
  • Scenario 2 — env present: explicit env vars are honoured without invoking profile path ✓
  • Scenario 3 — neither: env HOME=/tmp/empty env -u … bash scripts/test_sandbox_write.sh → clean ERROR: no credentials. Use 'direct auth login', …, exit 1 ✓
  • flake8 scripts/sandbox_write_live.py clean
  • black --check scripts/sandbox_write_live.py clean
  • bash -n scripts/test_sandbox_write.sh clean
  • Full WRITE_SANDBOX run on real sandbox (manual, after merge) — out of scope for this PR

🤖 Generated with Claude Code

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) <[email protected]>
Copilot AI review requested due to automatic review settings May 18, 2026 15:21
@axisrow axisrow added this to the 0.3.8 milestone May 18, 2026
axisrow added a commit that referenced this pull request May 18, 2026
…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) <[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

Restores parity with scripts/test_safe_commands.sh by letting the WRITE_SANDBOX runners fall back to an active direct auth login profile when YANDEX_DIRECT_TOKEN/YANDEX_DIRECT_LOGIN are not in the environment, so OAuth-profile users can run the sandbox-write smoke tests without manually exporting raw credentials.

Changes:

  • scripts/test_sandbox_write.sh: when env vars are missing, probe direct auth status for has_token=yes and eval-export credentials resolved via direct_cli.auth.get_credentials so child Python and direct --sandbox invocations inherit them.
  • scripts/sandbox_write_live.py::validate_environment(): defensive fallback to get_credentials(None, None) for direct Python entry, mutating os.environ so spawned subprocesses see the credentials; emits a clear error if no profile resolves either.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
scripts/test_sandbox_write.sh Shell-level fallback to auth profile via direct auth status + eval-export of resolved token/login.
scripts/sandbox_write_live.py validate_environment() now falls back to get_credentials() and populates os.environ for subprocesses.
Comments suppressed due to low confidence (1)

scripts/test_sandbox_write.sh:28

  • get_credentials(None, None) returns Tuple[str, Optional[str]]login can legitimately be None (see direct_cli/auth.py:600-702). When that happens this block prints export YANDEX_DIRECT_LOGIN=None, exporting the literal string "None" as the login, which the downstream direct --sandbox … calls will then attempt to use. Validate that both token and login are truthy before printing the export lines and exit with a clear error otherwise, mirroring the guard already present in sandbox_write_live.py:validate_environment().
  eval "$(python3 -c '
from direct_cli.auth import get_credentials
token, login = get_credentials(None, None)
print(f"export YANDEX_DIRECT_TOKEN={token}")
print(f"export YANDEX_DIRECT_LOGIN={login}")
')"

Comment thread scripts/test_sandbox_write.sh Outdated
Comment on lines +24 to +27
from direct_cli.auth import get_credentials
token, login = get_credentials(None, None)
print(f"export YANDEX_DIRECT_TOKEN={token}")
print(f"export YANDEX_DIRECT_LOGIN={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.

Addressed in ab927ea — shlex.quote() applied to both token and login, plus an explicit if not token or not login: sys.exit(...) guard with proper subshell-exit propagation via if ! resolved=$(...). Latest Claude review on the PR confirms both findings resolved.

axisrow added a commit that referenced this pull request May 18, 2026
…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) <[email protected]>
@axisrow

axisrow commented May 18, 2026

Copy link
Copy Markdown
Owner Author

Triage of the Codex adversarial review:

Finding 2 — Mutating probes inherit the read-only v4_live_read marker — fixed in 12b6625 on the #177 branch. The two write tests (wordstat / forecast lifecycle) are moved to `tests/test_integration_live_write.py` under the existing `integration_live_write` marker, with the file-level `skipif` already enforcing `YANDEX_DIRECT_LIVE_WRITE=1`. They are also rewritten to invoke the CLI via `_invoke_live` rather than calling `call_v4` natively, matching the convention of every other live-write test in that file. `tests/test_v4_live_contracts.py` is now strictly read-only.

Finding 1 — Active auth profile turns default test runs into live API calls — declined as an intentional project decision. The maintainer chose that bare `pytest` with an active `direct auth login` should hit Yandex Direct read endpoints. No `addopts` exclusion will be added.

Verification:

```bash
pytest --collect-only -m v4_live_read tests/test_v4_live_contracts.py # 9 tests, no lifecycle probes
pytest --collect-only -m integration_live_write tests/test_integration_live_write.py | grep -E "v4(wordstat|forecast)_lifecycle" # both present
env -u YANDEX_DIRECT_LIVE_WRITE pytest <…lifecycle…> # SKIPPED via file-level skipif
```

This PR (#179) itself is unchanged — it only touches `scripts/`. The marker / location fix lives entirely in #177.

@axisrow

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

Copy link
Copy Markdown

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


PR #179 Review

  • Read changed files
  • Analyze for critical issues
  • Post findings

Two critical issues found in scripts/test_sandbox_write.sh. The Python side (sandbox_write_live.py) is clean.


1. Shell injection via unquoted eval-export (security + correctness)

scripts/test_sandbox_write.sh lines 26–27:

eval "$(python3 -c '
from direct_cli.auth import get_credentials
token, login = get_credentials(None, None)
print(f"export YANDEX_DIRECT_TOKEN={token}")   # unquoted
print(f"export YANDEX_DIRECT_LOGIN={login}")   # unquoted
')"

Token and login values are interpolated directly into the shell strings passed to eval. OAuth tokens frequently contain +, /, = characters (base64 encoding). A token like y0_Ag==... emits:

export YANDEX_DIRECT_TOKEN=y0_Ag==...

== is valid in an unquoted assignment, but any token with embedded spaces, semicolons, backticks, or $() sequences would either silently truncate the value or execute arbitrary shell code from the token string. shlex.quote is the correct fix:

import shlex
print(f"export YANDEX_DIRECT_TOKEN={shlex.quote(token)}")
print(f"export YANDEX_DIRECT_LOGIN={shlex.quote(login)}")

Fix this →


2. None literal exported as login string (silent logic error)

get_credentials() is typed Tuple[str, Optional[str]] — login can legitimately be None (e.g., a profile created without a login). When that happens the shell block prints:

export YANDEX_DIRECT_LOGIN=None

This exports the string "None" into the environment. The direct --sandbox … subprocesses will then receive --login None (or equivalent), which is not a valid Yandex login and will produce confusing API errors rather than a clear credential failure.

sandbox_write_live.py:validate_environment() already handles this with if not token or not login: raise SystemExit(...) (line 1483), but the shell path has no equivalent guard. After the eval, add an explicit check:

if [[ -z "${YANDEX_DIRECT_LOGIN:-}" ]] || [[ "${YANDEX_DIRECT_LOGIN}" == "None" ]]; then
  echo "ERROR: auth profile resolved a token but no login. Re-run 'direct auth login'." >&2
  exit 1
fi

Or, more robustly, validate inside the Python snippet before printing the exports.

Fix this →


sandbox_write_live.py — no issues. The try/except + if not token or not login guard is correct and complete.

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) <[email protected]>
@axisrow

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

Copy link
Copy Markdown

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


PR #179 Review

  • Read changed files
  • Analyze for critical issues
  • Post findings

Both critical issues flagged in the previous review cycle are resolved in commit ab927ea. No new critical issues found.

Previous findings — resolved

Finding 1 — Shell injection via unquoted eval-exportFixed.
scripts/test_sandbox_write.sh lines 24 and 29–30 now use import shlex and shlex.quote(token) / shlex.quote(login), producing properly single-quoted shell strings before passing to eval "$resolved".

Finding 2 — None literal exported as loginFixed.
Lines 27–28 now validate before printing:

if not token or not login:
    sys.exit("ERROR: auth profile resolved a token but no login. Re-run \"direct auth login\".")

The sys.exit(str) writes to stderr (visible to user), the if ! resolved=$(...) catches the non-zero exit, and exit 1 follows. Correct.

sandbox_write_live.py:validate_environment() — clean

The try/except Exception + if not token or not login guard at lines 1478–1487 is correct and complete. os.environ mutation propagates to direct --sandbox … subprocesses as intended.

No further critical issues

The logic flow is sound: env vars → profile probe → credential resolution → validation → export. The set -euo pipefail + if ! idiom handles all exit-code paths correctly in the shell script.

PR is clear to merge from a correctness/security standpoint.

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) <[email protected]>
@axisrow
axisrow merged commit fcfbea2 into main May 18, 2026
6 checks passed
@axisrow
axisrow deleted the fix/sandbox-write-script-auth-profile branch May 18, 2026 16:27
axisrow added a commit that referenced this pull request May 19, 2026
…177)

* test(v4_live): add live contracts for v4forecast, v4tags, v4wordstat

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) <[email protected]>

* test(v4_live): gate wordstat/forecast lifecycle behind LIVE_WRITE opt-in

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) <[email protected]>

* 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) <[email protected]>

* test(v4_live): apply Claude review polish (next/single-arg/TODOs)

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) <[email protected]>

* test(v4_live): resolve credentials from direct auth profile when env 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) <[email protected]>

* test(v4_live): move write lifecycle probes to integration_live_write 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) <[email protected]>

* test(v4_live): record VCR cassettes for v4wordstat/v4forecast lifecycle

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.

* test(v4_live): move v4 report lifecycle to opt-in tier, add orphan store

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) <[email protected]>

* test(v5): rename test_integration_live_write to test_v5_live_write

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) <[email protected]>

* test: broaden exception handlers in test bootstrap and orphan store

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) <[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.

bug(scripts): test_sandbox_write.sh ignores auth profile, requires raw env vars

2 participants