Skip to content

Fix vacuous test for private Hugging Face repo download without a token - #539

Open
MaxGhenis wants to merge 2 commits into
masterfrom
fix/hf-no-token-test-vacuous
Open

Fix vacuous test for private Hugging Face repo download without a token#539
MaxGhenis wants to merge 2 commits into
masterfrom
fix/hf-no-token-test-vacuous

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

TestHuggingFaceDownload::test_download_private_repo_no_token in tests/core/tools/test_hugging_face.py passed vacuously. It did:

with pytest.raises(Exception):
    download_huggingface_dataset(test_repo, test_filename, test_version, test_dir)
    mock_download.assert_not_called()

download_huggingface_dataset does not raise (the download is mocked), so control reached assert_not_called(), which raised AssertionError because the download was called, and that AssertionError satisfied pytest.raises(Exception). The test asserted nothing about the code. It has been this way since it was added in a7b1e51 (Dec 2024).

Intended behaviour

Read from download_huggingface_dataset and get_or_prompt_hf_token, and from #422:

  • With HUGGING_FACE_TOKEN unset or empty and stdin not a TTY, get_or_prompt_hf_token() returns None rather than prompting (Handle empty HUGGING_FACE_TOKEN gracefully #422 made this deliberate so Dependabot CI without secrets does not hang or send Bearer ).
  • download_huggingface_dataset passes that None through to hf_hub_download and does not raise.
  • huggingface_hub (1.4.1, utils._headers.get_token_to_send) treats token=None as "use the cached token if any": HF_TOKEN or the hf auth login file. If that is also missing, the Hub returns its own 401/GatedRepoError.

So a hard failure in core when no HUGGING_FACE_TOKEN is set would break users who are logged in via hf auth login and CI that relies on HF_TOKEN. This PR asserts the pass-through behaviour rather than adding a hard failure.

Change

Test-only. The rewritten test, parametrised over HUGGING_FACE_TOKEN unset, HUGGING_FACE_TOKEN="", and huggingface_hub's own HF_TOKEN set with HUGGING_FACE_TOKEN unset, exercises the real get_or_prompt_hf_token with os.isatty patched to False and asserts:

  • getpass is never called,
  • hf_hub_download is called exactly once with token=None (and the other expected arguments), so in the HF_TOKEN case core leaves resolving that token to huggingface_hub,
  • its return value is passed through,
  • no exception is raised.

Changelog fragment: changelog.d/fix-hf-no-token-test.fixed.md.

Mutation check

Each mutation was applied to policyengine_core/tools/hugging_face.py in turn, then both the old test (from master) and the new test were run:

Mutation Old test New test
Raise RuntimeError when the resolved token is None passed failed (3/3)
Pass "" instead of None to hf_hub_download passed failed (3/3)
Prompt via getpass even when non-interactive passed failed (3/3, on mock_getpass.assert_not_called())
Core forwards HF_TOKEN itself instead of leaving it to huggingface_hub passed failed (1/3, the hf-token-only case)
Never call hf_hub_download at all failed failed (3/3)

The last row is the tell: the old test was not just vacuous but inverted. It passed whenever the download raised or hf_hub_download was called, and failed only in the one state its name described.

Checks run

  • uvx ruff format . (no changes) and uvx ruff check . (clean)
  • uv run pytest tests/core/tools/test_hugging_face.py -v: 17 passed
  • Full uv run pytest tests locally: 697 passed, 4 skipped, 1 xfailed
  • An adversarial review pass (independent agent, read-only, reproduced every mutation above from the installed huggingface_hub 1.4.1 source) returned approve; its two nits (getpass mock return value, wording of "vacuous") are folded into the second commit.

Not run: make documentation and make build (no docs or packaging changes).

Out of scope

The review also suggested a warnings.warn in download_huggingface_dataset when a private or gated repo resolves token=None, so the eventual 401 is easier to diagnose than in #529. That is a behaviour change and belongs in its own PR.

Note on #538

#538 ("Send HUGGING_FACE_TOKEN for public but gated Hugging Face repos") adds tests immediately after the block this PR rewrites, so whichever of the two merges second will need a rebase. The two do not overlap in intent: #538 adds a gated-repo variant of the same non-interactive-no-token check; this PR fixes the private-repo variant that already existed.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits September 3, 2026 15:26
test_download_private_repo_no_token wrapped mock_download.assert_not_called()
inside pytest.raises(Exception). download_huggingface_dataset never raises
when hf_hub_download is mocked, so control reached assert_not_called(), whose
AssertionError (the download had been called) satisfied pytest.raises. The
test therefore passed regardless of what the code did, from its first commit
(a7b1e51) onward.

The intended behaviour, per #422, is that a private repo with no token in a
non-interactive environment passes token=None through to hf_hub_download
without prompting or raising; huggingface_hub then falls back to its own
cached token (HF_TOKEN or the login file) and raises its own error if that is
absent too. Rewrite the test to assert exactly that, parametrised over an
unset and an empty HUGGING_FACE_TOKEN.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Give the getpass mock a return value so the "prompts when
  non-interactive" regression fails on mock_getpass.assert_not_called()
  instead of on a TypeError from storing a MagicMock in os.environ.
- Add an HF_TOKEN-only parametrisation: core still passes token=None,
  pinning that resolving huggingface_hub's own cached token is
  huggingface_hub's job, not core's.
- Assert the hf_hub_download return value is passed through.
- Say precisely why the old test passed: control reached
  assert_not_called() only because the download had been called, so the
  test passed on the opposite of what its name claimed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant