Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

610 lines
20 KiB
Python

from __future__ import annotations
import os
import stat
import keyring
import pytest
from config.llm_credentials import resolve_env_credential
from surfaces.cli.wizard.config import PROVIDER_BY_VALUE
from surfaces.cli.wizard.env_sync import (
_is_sensitive_env_key,
sync_env_secret,
sync_env_values,
sync_provider_env,
sync_reasoning_model_env,
)
from surfaces.cli.wizard.store import load_local_config
from tests.shared.keyring_backend import MemoryKeyring
_SKIP_AS_ROOT = not hasattr(os, "getuid") or os.getuid() == 0
@pytest.fixture(autouse=True)
def _redirect_wizard_store(tmp_path, monkeypatch) -> None:
"""Keep sync_provider_env store updates off the developer's ~/.opensre."""
monkeypatch.setattr(
"surfaces.cli.wizard.store.get_store_path",
lambda: tmp_path / "opensre.json",
)
@pytest.mark.parametrize(
"key",
[
# Suffix-style sensitive keys (existing behavior).
"GITLAB_ACCESS_TOKEN",
"ANTHROPIC_API_KEY",
"AWS_SECRET_ACCESS_KEY",
"DB_PASSWORD",
# Bare sensitive keys (previously slipped past the suffix-only filter
# and reached plain-text .env, which CodeQL flagged as alert #1019).
"PASSWORD",
"TOKEN",
"SECRET",
"KEY",
"APIKEY",
"CREDENTIAL",
# Substring-based sensitives.
"DATABASE_CONNECTION_STRING",
],
)
def test_is_sensitive_env_key_marks_secrets(key: str) -> None:
assert _is_sensitive_env_key(key) is True
@pytest.mark.parametrize(
"key",
[
# Non-secret configuration env vars must stay writable to .env.
"LLM_PROVIDER",
"OPENAI_REASONING_MODEL",
"OPENAI_MODEL",
"GITLAB_BASE_URL",
"ENV",
# Terminal token is not in the sensitive set even though "TOKEN"
# appears as a substring — the limit count is not itself a secret.
"OPENAI_TOKEN_LIMIT",
# Explicit exception: a public discord key is not sensitive.
"DISCORD_PUBLIC_KEY",
],
)
def test_is_sensitive_env_key_leaves_non_secrets(key: str) -> None:
assert _is_sensitive_env_key(key) is False
def test_sync_provider_env_updates_provider_specific_keys(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("OPENAI_REASONING_MODEL", raising=False)
monkeypatch.delenv("OPENAI_MODEL", raising=False)
env_path = tmp_path / ".env"
env_path.write_text(
"ENV=development\n"
"LLM_PROVIDER=anthropic\n"
"ANTHROPIC_API_KEY=legacy-anthropic\n"
"OPENAI_API_KEY=old-key\n",
encoding="utf-8",
)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5-mini",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "ENV=development\n" in content
assert content.count("LLM_PROVIDER=") == 1
assert "LLM_PROVIDER=openai\n" in content
assert "OPENAI_API_KEY=" not in content
assert "ANTHROPIC_API_KEY=" not in content
assert "OPENAI_REASONING_MODEL=gpt-5-mini\n" in content
assert "OPENAI_MODEL=gpt-5-mini\n" in content
def test_sync_provider_env_strips_integration_fallback_secrets(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("OPENAI_REASONING_MODEL", raising=False)
monkeypatch.delenv("OPENAI_MODEL", raising=False)
env_path = tmp_path / ".env"
env_path.write_text(
"TELEGRAM_BOT_TOKEN=manual-fallback-token\nLLM_PROVIDER=anthropic\n",
encoding="utf-8",
)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5-mini",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "TELEGRAM_BOT_TOKEN=" not in content
assert "LLM_PROVIDER=openai\n" in content
def test_sync_provider_env_updates_wizard_store(tmp_path, monkeypatch) -> None:
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("OPENAI_REASONING_MODEL", raising=False)
monkeypatch.delenv("OPENAI_MODEL", raising=False)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5-mini",
env_path=env_path,
)
stored = load_local_config(store_path)
local = stored["targets"]["local"]
assert local["provider"] == "openai"
assert local["model"] == "gpt-5-mini"
assert local["api_key_env"] == "OPENAI_API_KEY"
assert local["model_env"] == "OPENAI_REASONING_MODEL"
def test_sync_reasoning_model_env_updates_wizard_store(tmp_path, monkeypatch) -> None:
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setenv("LLM_PROVIDER", "openai")
sync_reasoning_model_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
env_path=env_path,
)
stored = load_local_config(store_path)
local = stored["targets"]["local"]
assert local["provider"] == "openai"
assert local["model"] == "gpt-5.4-mini"
assert env_path.read_text(encoding="utf-8") == (
"OPENAI_REASONING_MODEL=gpt-5.4-mini\nOPENAI_MODEL=gpt-5.4-mini\n"
)
def test_sync_provider_env_appends_to_file_without_final_newline(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("OPENAI_REASONING_MODEL", raising=False)
monkeypatch.delenv("OPENAI_MODEL", raising=False)
env_path = tmp_path / ".env"
env_path.write_text(
"ENV=development\n"
"LLM_PROVIDER=anthropic\n"
"ANTHROPIC_API_KEY=legacy-anthropic\n"
"TEST_ENV=no-new-line",
encoding="utf-8",
)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5-mini",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert content.endswith("OPENAI_MODEL=gpt-5-mini\n")
assert "LLM_PROVIDER=openai\n" in content
assert "ANTHROPIC_API_KEY=" not in content
def test_sync_provider_env_codex_writes_codex_model(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("CODEX_MODEL", raising=False)
env_path = tmp_path / ".env"
env_path.write_text("LLM_PROVIDER=anthropic\n", encoding="utf-8")
sync_provider_env(
provider=PROVIDER_BY_VALUE["codex"],
model="",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=codex\n" in content
assert "CODEX_MODEL=\n" in content
def test_sync_provider_env_openai_oauth_writes_auth_method_and_codex_model(
tmp_path, monkeypatch
) -> None:
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("LLM_AUTH_METHOD", raising=False)
monkeypatch.delenv("CODEX_MODEL", raising=False)
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=anthropic\nANTHROPIC_REASONING_MODEL=claude-opus-4-7\n",
encoding="utf-8",
)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="",
model_provider=PROVIDER_BY_VALUE["codex"],
auth_method="oauth",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=openai\n" in content
assert "LLM_AUTH_METHOD=oauth\n" in content
assert "CODEX_MODEL=\n" in content
assert "ANTHROPIC_REASONING_MODEL=" not in content
assert os.environ["LLM_PROVIDER"] == "openai"
assert os.environ["LLM_AUTH_METHOD"] == "oauth"
assert os.environ["CODEX_MODEL"] == ""
def test_sync_provider_env_gemini_cli_writes_model(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("LLM_PROVIDER", raising=False)
monkeypatch.delenv("GEMINI_CLI_MODEL", raising=False)
env_path = tmp_path / ".env"
env_path.write_text("LLM_PROVIDER=anthropic\n", encoding="utf-8")
sync_provider_env(
provider=PROVIDER_BY_VALUE["gemini-cli"],
model="",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=gemini-cli\n" in content
assert "GEMINI_CLI_MODEL=\n" in content
def test_sync_provider_env_removes_stale_toolcall_and_classification_keys(
tmp_path, monkeypatch
) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\n"
"OPENAI_REASONING_MODEL=gpt-5.4-mini\n"
"OPENAI_MODEL=gpt-5.4-mini\n"
"OPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n"
"OPENAI_CLASSIFICATION_MODEL=gpt-5.4-mini\n"
"CODEX_MODEL=\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_REASONING_MODEL", "gpt-5.4-mini")
monkeypatch.setenv("OPENAI_TOOLCALL_MODEL", "gpt-5.4-mini")
sync_provider_env(
provider=PROVIDER_BY_VALUE["codex"],
model="gpt-5.4-mini",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=codex\n" in content
assert "CODEX_MODEL=gpt-5.4-mini\n" in content
assert "OPENAI_TOOLCALL_MODEL=" not in content
assert "OPENAI_CLASSIFICATION_MODEL=" not in content
assert "OPENAI_REASONING_MODEL=" not in content
assert "OPENAI_TOOLCALL_MODEL" not in os.environ
def test_sync_provider_env_loads_preserved_keys_from_env_file(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\n"
"OPENAI_REASONING_MODEL=gpt-5.4-mini\n"
"OPENAI_MODEL=gpt-5.4-mini\n"
"OPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n"
"OPENAI_CLASSIFICATION_MODEL=gpt-5.4-mini\n",
encoding="utf-8",
)
monkeypatch.delenv("OPENAI_TOOLCALL_MODEL", raising=False)
monkeypatch.delenv("OPENAI_CLASSIFICATION_MODEL", raising=False)
monkeypatch.setenv("LLM_PROVIDER", "anthropic")
monkeypatch.setenv("OPENAI_REASONING_MODEL", "stale")
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
env_path=env_path,
)
assert os.environ["LLM_PROVIDER"] == "openai"
assert os.environ["OPENAI_REASONING_MODEL"] == "gpt-5.4-mini"
assert os.environ["OPENAI_TOOLCALL_MODEL"] == "gpt-5.4-mini"
assert os.environ["OPENAI_CLASSIFICATION_MODEL"] == "gpt-5.4-mini"
def test_sync_provider_env_preserves_active_provider_toolcall_key(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\n"
"OPENAI_REASONING_MODEL=gpt-5.4-mini\n"
"OPENAI_MODEL=gpt-5.4-mini\n"
"OPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_REASONING_MODEL", "gpt-5.4-mini")
monkeypatch.setenv("OPENAI_TOOLCALL_MODEL", "gpt-5.4-mini")
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "OPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n" in content
assert os.environ.get("OPENAI_TOOLCALL_MODEL") == "gpt-5.4-mini"
def test_sync_provider_env_updates_os_environ(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\nOPENAI_REASONING_MODEL=gpt-5.4-mini\nOPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_REASONING_MODEL", "gpt-5.4-mini")
monkeypatch.setenv("OPENAI_TOOLCALL_MODEL", "gpt-5.4-mini")
sync_provider_env(
provider=PROVIDER_BY_VALUE["codex"],
model="gpt-5.4-mini",
env_path=env_path,
)
assert os.environ["LLM_PROVIDER"] == "codex"
assert os.environ["CODEX_MODEL"] == "gpt-5.4-mini"
assert "OPENAI_TOOLCALL_MODEL" not in os.environ
assert "OPENAI_REASONING_MODEL" not in os.environ
def test_sync_provider_env_skips_empty_preserved_values_in_os_environ(
tmp_path, monkeypatch
) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\n"
"OPENAI_REASONING_MODEL=gpt-5.4-mini\n"
"OPENAI_MODEL=gpt-5.4-mini\n"
"OPENAI_TOOLCALL_MODEL=\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_REASONING_MODEL", "gpt-5.4-mini")
monkeypatch.delenv("OPENAI_TOOLCALL_MODEL", raising=False)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
env_path=env_path,
)
assert os.environ["OPENAI_REASONING_MODEL"] == "gpt-5.4-mini"
assert "OPENAI_TOOLCALL_MODEL" not in os.environ
def test_sync_provider_env_skips_empty_toolcall_model_override(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\n"
"OPENAI_REASONING_MODEL=gpt-5.4-mini\n"
"OPENAI_MODEL=gpt-5.4-mini\n"
"OPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_TOOLCALL_MODEL", "gpt-5.4-mini")
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
toolcall_model="",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "OPENAI_TOOLCALL_MODEL=gpt-5.4-mini\n" in content
assert os.environ["OPENAI_TOOLCALL_MODEL"] == "gpt-5.4-mini"
def test_sync_provider_env_writes_toolcall_model_atomically(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"LLM_PROVIDER=openai\n"
"OPENAI_REASONING_MODEL=gpt-5.4-mini\n"
"OPENAI_MODEL=gpt-5.4-mini\n"
"OPENAI_TOOLCALL_MODEL=old-toolcall\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_REASONING_MODEL", "gpt-5.4-mini")
monkeypatch.setenv("OPENAI_TOOLCALL_MODEL", "old-toolcall")
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
toolcall_model="gpt-5.4-nano",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "OPENAI_REASONING_MODEL=gpt-5.4-mini\n" in content
assert "OPENAI_TOOLCALL_MODEL=gpt-5.4-nano\n" in content
assert os.environ["OPENAI_REASONING_MODEL"] == "gpt-5.4-mini"
assert os.environ["OPENAI_TOOLCALL_MODEL"] == "gpt-5.4-nano"
def test_sync_provider_env_sets_and_clears_azure_litellm_transport(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
azure = PROVIDER_BY_VALUE["azure-openai"]
env_path.write_text(
"LLM_PROVIDER=anthropic\n"
"OPENSRE_LLM_TRANSPORT=litellm\n"
"AZURE_OPENAI_BASE_URL=https://example.openai.azure.com\n",
encoding="utf-8",
)
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-5.4-mini",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=openai\n" in content
assert "OPENSRE_LLM_TRANSPORT=" not in content
sync_provider_env(
provider=azure,
model="gpt-5.4-mini",
extra_env={
"AZURE_OPENAI_BASE_URL": "https://example.openai.azure.com",
"AZURE_OPENAI_API_VERSION": "2024-10-21",
},
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "LLM_PROVIDER=azure-openai\n" in content
assert "OPENSRE_LLM_TRANSPORT=litellm\n" in content
assert "AZURE_OPENAI_API_VERSION=2024-10-21\n" in content
assert os.environ["OPENSRE_LLM_TRANSPORT"] == "litellm"
def test_sync_provider_env_preserves_azure_endpoint_on_model_switch(tmp_path, monkeypatch) -> None:
env_path = tmp_path / ".env"
azure = PROVIDER_BY_VALUE["azure-openai"]
env_path.write_text(
"LLM_PROVIDER=azure-openai\n"
"OPENSRE_LLM_TRANSPORT=litellm\n"
"AZURE_OPENAI_BASE_URL=https://example.openai.azure.com\n"
"AZURE_OPENAI_API_VERSION=2024-10-21\n"
"AZURE_OPENAI_REASONING_MODEL=gpt-5.4-mini\n",
encoding="utf-8",
)
sync_provider_env(
provider=azure,
model="gpt-5.4",
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "AZURE_OPENAI_BASE_URL=https://example.openai.azure.com\n" in content
assert "AZURE_OPENAI_API_VERSION=2024-10-21\n" in content
assert "AZURE_OPENAI_REASONING_MODEL=gpt-5.4\n" in content
assert os.environ["AZURE_OPENAI_BASE_URL"] == "https://example.openai.azure.com"
@pytest.mark.skipif(_SKIP_AS_ROOT, reason="root bypasses file permission checks")
def test_sync_provider_env_permission_error(tmp_path) -> None:
env_path = tmp_path / ".env"
env_path.write_text("LLM_PROVIDER=anthropic\n", encoding="utf-8")
env_path.chmod(stat.S_IRUSR) # read-only
try:
with pytest.raises(PermissionError, match="permission denied"):
sync_provider_env(
provider=PROVIDER_BY_VALUE["openai"],
model="gpt-4o",
env_path=env_path,
)
finally:
env_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
def test_sync_env_values_rejects_sensitive_keys(tmp_path) -> None:
env_path = tmp_path / ".env"
env_path.write_text("FOO=bar\n", encoding="utf-8")
with pytest.raises(ValueError, match="sync_env_secret"):
sync_env_values({"GITLAB_ACCESS_TOKEN": "secret"}, env_path=env_path)
def test_sync_env_values_routes_secrets_to_keyring(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("GITLAB_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
previous_backend = keyring.get_keyring()
keyring.set_keyring(MemoryKeyring())
try:
env_path = tmp_path / ".env"
env_path.write_text(
"GITLAB_BASE_URL=https://gitlab.example.com\nGITLAB_ACCESS_TOKEN=legacy-plaintext\n",
encoding="utf-8",
)
sync_env_secret("GITLAB_ACCESS_TOKEN", "gl-secret-token")
sync_env_values(
{"GITLAB_BASE_URL": "https://gitlab.corp.com"},
env_path=env_path,
)
content = env_path.read_text(encoding="utf-8")
assert "GITLAB_BASE_URL=https://gitlab.corp.com\n" in content
assert "GITLAB_ACCESS_TOKEN=" not in content
assert resolve_env_credential("GITLAB_ACCESS_TOKEN") == "gl-secret-token"
finally:
keyring.set_keyring(previous_backend)
@pytest.mark.skipif(_SKIP_AS_ROOT, reason="root bypasses file permission checks")
def test_sync_env_values_permission_error(tmp_path) -> None:
env_path = tmp_path / ".env"
env_path.write_text("FOO=bar\n", encoding="utf-8")
env_path.chmod(stat.S_IRUSR)
try:
with pytest.raises(PermissionError, match="permission denied"):
sync_env_values({"FOO": "baz"}, env_path=env_path)
finally:
env_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
def test_strip_keyring_backed_secret_lines_removes_all_sensitive_lines() -> None:
from surfaces.cli.wizard.env_sync import _strip_keyring_backed_secret_lines
lines = [
"TELEGRAM_BOT_TOKEN=fallback\n",
"DD_API_KEY=in-keyring\n",
"DD_SITE=old\n",
]
kept = _strip_keyring_backed_secret_lines(lines)
assert kept == ["DD_SITE=old\n"]
def test_sync_env_values_strips_telegram_bot_token_fallback(tmp_path) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"TELEGRAM_BOT_TOKEN=manual-fallback-token\nDD_SITE=old\n",
encoding="utf-8",
)
sync_env_values({"DD_SITE": "datadoghq.eu"}, env_path=env_path)
content = env_path.read_text(encoding="utf-8")
assert "TELEGRAM_BOT_TOKEN=" not in content
assert "DD_SITE=datadoghq.eu" in content
def test_sync_env_values_strips_multiple_fallback_secrets(tmp_path) -> None:
env_path = tmp_path / ".env"
env_path.write_text(
"DD_API_KEY=datadog-api\nDD_APP_KEY=datadog-app\n",
encoding="utf-8",
)
sync_env_values({"DD_SITE": "datadoghq.com"}, env_path=env_path)
content = env_path.read_text(encoding="utf-8")
assert "DD_API_KEY=" not in content
assert "DD_APP_KEY=" not in content
assert "DD_SITE=datadoghq.com" in content
def test_sync_env_values_empty_update_strips_fallback_secrets(tmp_path) -> None:
"""Wizard paths call ``sync_env_values({})`` after integration setup."""
env_path = tmp_path / ".env"
env_path.write_text("TELEGRAM_BOT_TOKEN=manual-fallback-token\n", encoding="utf-8")
sync_env_values({}, env_path=env_path)
assert env_path.read_text(encoding="utf-8") == ""