mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(models): decode CLI credential files as UTF-8 * fix(doctor): accept BOM CLI credentials consistently
116 lines
4.5 KiB
Python
116 lines
4.5 KiB
Python
"""CLI credential files must decode independently of the host locale."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import doctor
|
|
import pytest
|
|
|
|
from deerflow.models.credential_loader import load_claude_code_credential, load_codex_cli_credential
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolate_credential_sources(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
for name in (
|
|
"ANTHROPIC_API_KEY",
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
"ANTHROPIC_AUTH_TOKEN",
|
|
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
|
"CLAUDE_CODE_CREDENTIALS_PATH",
|
|
"CODEX_AUTH_PATH",
|
|
):
|
|
monkeypatch.delenv(name, raising=False)
|
|
|
|
|
|
@pytest.fixture(params=["claude", "codex", "codex-legacy"])
|
|
def credential_file(request, tmp_path, monkeypatch):
|
|
path = tmp_path / "credentials.json"
|
|
if request.param == "claude":
|
|
monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(path))
|
|
payload = {"claudeAiOauth": {"accessToken": "synthetic-token"}}
|
|
loader = load_claude_code_credential
|
|
else:
|
|
monkeypatch.setenv("CODEX_AUTH_PATH", str(path))
|
|
tokens = {"access_token": "synthetic-token", "account_id": "synthetic-account"}
|
|
payload = {"tokens": tokens} if request.param == "codex" else tokens
|
|
loader = load_codex_cli_credential
|
|
return path, payload, loader
|
|
|
|
|
|
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
|
|
def test_cli_credentials_accept_utf8_with_or_without_bom(credential_file, encoding):
|
|
path, payload, loader = credential_file
|
|
path.write_text(json.dumps(payload), encoding=encoding)
|
|
|
|
credential = loader()
|
|
|
|
assert credential is not None
|
|
assert credential.access_token == "synthetic-token"
|
|
if hasattr(credential, "account_id"):
|
|
assert credential.account_id == "synthetic-account"
|
|
|
|
|
|
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
|
|
def test_doctor_accepts_the_same_utf8_credential_files(credential_file, encoding):
|
|
path, payload, loader = credential_file
|
|
path.write_text(json.dumps(payload), encoding=encoding)
|
|
provider = "claude_provider:ClaudeChatModel" if loader is load_claude_code_credential else "openai_codex_provider:CodexChatModel"
|
|
config = path.parent / "config.yaml"
|
|
config.write_text(f"config_version: 5\nmodels:\n - name: synthetic\n use: deerflow.models.{provider}\n model: synthetic\n", encoding="utf-8")
|
|
|
|
assert loader() is not None
|
|
results = doctor.check_llm_auth(config)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].status == "ok"
|
|
assert results[0].detail == str(path)
|
|
assert "synthetic-token" not in results[0].detail
|
|
|
|
|
|
def test_cli_credentials_do_not_use_the_host_text_encoding(credential_file, monkeypatch):
|
|
path, payload, loader = credential_file
|
|
# U+201D contains byte 0x9d in UTF-8, which is undefined in cp1252.
|
|
# A valid JSON file can contain Unicode metadata unrelated to authentication.
|
|
payload["label"] = "开发者”"
|
|
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
original_open = Path.open
|
|
|
|
def legacy_locale_open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
|
|
if self == path and "b" not in mode and encoding in (None, "locale"):
|
|
encoding = "cp1252"
|
|
return original_open(self, mode, buffering, encoding, errors, newline)
|
|
|
|
monkeypatch.setattr(Path, "open", legacy_locale_open)
|
|
|
|
credential = loader()
|
|
|
|
assert credential is not None
|
|
assert credential.access_token == "synthetic-token"
|
|
|
|
|
|
@pytest.mark.parametrize("content", [b"\xff\xfeinvalid", b'{"access_token": "synthetic-token"}\xff', b"not JSON"])
|
|
def test_unreadable_credential_files_degrade_to_no_credential(credential_file, content, caplog):
|
|
path, _, loader = credential_file
|
|
path.write_bytes(content)
|
|
|
|
assert loader() is None
|
|
assert "Failed to read" in caplog.text
|
|
assert "synthetic-token" not in caplog.text
|
|
|
|
|
|
def test_claude_falls_back_after_invalid_utf8_override(tmp_path, monkeypatch):
|
|
override = tmp_path / "invalid.json"
|
|
override.write_bytes(b"\xff")
|
|
monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(override))
|
|
default = tmp_path / ".claude" / ".credentials.json"
|
|
default.parent.mkdir()
|
|
default.write_text(json.dumps({"claudeAiOauth": {"accessToken": "fallback-token"}}), encoding="utf-8")
|
|
|
|
credential = load_claude_code_credential()
|
|
|
|
assert credential is not None
|
|
assert credential.access_token == "fallback-token"
|
|
assert credential.source == "claude-cli-file"
|