deer-flow/backend/tests/test_claude_fd_encoding.py

105 lines
4.2 KiB
Python

import json
import logging
import os
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
import pytest
from deerflow.models import credential_loader
from deerflow.models.claude_provider import ClaudeChatModel
@pytest.fixture(autouse=True)
def _isolate_credentials(tmp_path, monkeypatch):
for name in (
"CLAUDE_CODE_OAUTH_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
"CLAUDE_CODE_CREDENTIALS_PATH",
):
monkeypatch.delenv(name, raising=False)
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.setattr(credential_loader, "_fd_secret_cache", {})
@contextmanager
def _handoff(payload: bytes, monkeypatch) -> Iterator[int]:
read_fd, write_fd = os.pipe()
try:
try:
os.write(write_fd, payload)
finally:
os.close(write_fd)
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR", str(read_fd))
yield read_fd
finally:
os.close(read_fd)
def _credential_file(path: Path) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"claudeAiOauth": {"accessToken": "sk-ant-oat01-file-fallback"}}), encoding="utf-8")
return path
@pytest.mark.parametrize(
"payload",
[b"sk-ant-oat01-invalid-byte\xff", b"\xff\xfe" + "sk-ant-oat01-utf16".encode("utf-16-le"), b"sk-ant-oat01-truncated\xe2\x82"],
ids=["invalid-byte", "utf16", "truncated-utf8"],
)
@pytest.mark.parametrize("fallback", ["none", "override", "default"])
def test_undecodable_descriptor_skips_to_file_credentials(tmp_path, monkeypatch, caplog, payload, fallback):
if fallback == "override":
monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(_credential_file(tmp_path / "override.json")))
elif fallback == "default":
_credential_file(tmp_path / ".claude" / ".credentials.json")
with caplog.at_level(logging.WARNING, logger=credential_loader.__name__), _handoff(payload, monkeypatch) as read_fd:
# A failed handoff must not strand either the first model or later loads.
credentials = [credential_loader.load_claude_code_credential() for _ in range(2)]
os.fstat(read_fd) # The loader does not own or close the descriptor.
if fallback == "none":
assert credentials == [None, None]
else:
assert all(cred is not None and cred.access_token == "sk-ant-oat01-file-fallback" and cred.source == "claude-cli-file" for cred in credentials)
assert credential_loader._fd_secret_cache == {}
assert "Failed to read CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR" in caplog.text
assert "sk-ant-oat01" not in caplog.text
def test_claude_models_use_file_fallback_after_undecodable_descriptor(tmp_path, monkeypatch):
_credential_file(tmp_path / ".claude" / ".credentials.json")
with _handoff(b"sk-ant-oat01-model-handoff\xff", monkeypatch):
models = [ClaudeChatModel(model="claude-sonnet-4-6") for _ in range(2)]
for model in models:
assert model._is_oauth is True
assert model._client.api_key is None
assert model._client.auth_token == "sk-ant-oat01-file-fallback"
def test_valid_descriptor_still_wins_over_file_and_reuses_cached_token(tmp_path, monkeypatch):
_credential_file(tmp_path / ".claude" / ".credentials.json")
with _handoff(b" sk-ant-oat01-valid-handoff\n", monkeypatch):
credentials = [credential_loader.load_claude_code_credential() for _ in range(2)]
assert all(cred is not None and cred.access_token == "sk-ant-oat01-valid-handoff" and cred.source == "claude-cli-fd" for cred in credentials)
def test_direct_environment_token_precedes_undecodable_descriptor(monkeypatch, caplog):
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "sk-ant-oat01-direct")
with _handoff(b"sk-ant-oat01-bad-handoff\xff", monkeypatch) as read_fd:
cred = credential_loader.load_claude_code_credential()
assert os.read(read_fd, 1024) == b"sk-ant-oat01-bad-handoff\xff"
assert cred is not None and cred.access_token == "sk-ant-oat01-direct" and cred.source == "claude-cli-env"
assert "Failed to read" not in caplog.text