Merge remote-tracking branch 'origin/main' into feat/removed-flag-env-migration
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 14s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 17s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 14s
CI / Catalog signature (pull_request) Successful in 9s
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 14s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 17s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 14s
CI / Catalog signature (pull_request) Successful in 9s
# Conflicts: # tests/test_core.py
This commit is contained in:
@@ -3206,3 +3206,175 @@ def test_ssh_membermatters_end_to_end():
|
||||
]
|
||||
assert new["env"] == {"SSH_MCP_PASSWORD": "topsecret"}
|
||||
assert notes
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Move to environment variable (issue #83)
|
||||
# --------------------------------------------------------------------------- #
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
("API_KEY", "API_KEY"),
|
||||
("api-key", "API_KEY"),
|
||||
("x.y z", "X_Y_Z"),
|
||||
("2fa", "_2FA"),
|
||||
("", "VAR"),
|
||||
("***", "VAR"),
|
||||
("clé", "CL_"), # non-ASCII becomes _
|
||||
],
|
||||
)
|
||||
def test_sanitize_env_var_name(raw, expected):
|
||||
assert c.sanitize_env_var_name(raw) == expected
|
||||
|
||||
|
||||
def test_shell_export_lines_quote_safely():
|
||||
lines = c.shell_export_lines("TOKEN", "ab'cd")
|
||||
assert lines["posix"] == "export TOKEN='ab'\\''cd'"
|
||||
assert lines["windows"] == 'setx TOKEN "ab\'cd"'
|
||||
|
||||
|
||||
def test_can_move_gate_requires_secret_and_expanding_client():
|
||||
desktop = c.Profile(label="d", path="/x/Claude/claude_desktop_config.json", config_exists=True)
|
||||
code = c.Profile(label="c", path=Path.home() / ".claude.json", config_exists=True)
|
||||
# real secret on an expanding client -> offer
|
||||
assert c.can_move_value_to_env_ref("API_KEY", "ghp_abc", code) is True
|
||||
assert c.can_move_value_to_env_ref("API_KEY", "ghp_abc", None) is True
|
||||
# non-secret key -> no
|
||||
assert c.can_move_value_to_env_ref("REGION", "us-east-1", code) is False
|
||||
# already a reference -> no
|
||||
assert c.can_move_value_to_env_ref("API_KEY", "${API_KEY}", code) is False
|
||||
# non-expanding client (Claude Desktop) -> refuse even a real secret
|
||||
assert c.can_move_value_to_env_ref("API_KEY", "ghp_abc", desktop) is False
|
||||
|
||||
|
||||
def test_move_env_value_replaces_with_reference_and_returns_secret():
|
||||
data = {"command": "x", "env": {"API_KEY": "ghp_secret", "REGION": "us"}}
|
||||
conv = c.move_value_to_env_ref(data, field="env", key="API_KEY")
|
||||
assert conv is not None
|
||||
assert conv.var_name == "API_KEY"
|
||||
assert conv.reference == "${API_KEY}"
|
||||
assert conv.secret == "ghp_secret"
|
||||
assert conv.data["env"]["API_KEY"] == "${API_KEY}"
|
||||
# non-secret row untouched
|
||||
assert conv.data["env"]["REGION"] == "us"
|
||||
# input never mutated
|
||||
assert data["env"]["API_KEY"] == "ghp_secret"
|
||||
|
||||
|
||||
def test_move_derives_and_sanitises_var_name_from_key():
|
||||
data = {"headers": {"x-api-key": "sekret"}}
|
||||
conv = c.move_value_to_env_ref(data, field="headers", key="x-api-key")
|
||||
assert conv.var_name == "X_API_KEY"
|
||||
assert conv.data["headers"]["x-api-key"] == "${X_API_KEY}"
|
||||
|
||||
|
||||
def test_move_honours_explicit_var_name():
|
||||
data = {"env": {"tok": "sekret"}}
|
||||
conv = c.move_value_to_env_ref(data, field="env", key="tok", var_name="GITHUB_TOKEN")
|
||||
assert conv.reference == "${GITHUB_TOKEN}"
|
||||
assert conv.data["env"]["tok"] == "${GITHUB_TOKEN}"
|
||||
|
||||
|
||||
def test_move_args_by_index():
|
||||
data = {"command": "x", "args": ["--token", "ghp_secret"]}
|
||||
conv = c.move_value_to_env_ref(data, field="args", index=1, var_name="GH_TOKEN")
|
||||
assert conv.secret == "ghp_secret"
|
||||
assert conv.data["args"] == ["--token", "${GH_TOKEN}"]
|
||||
|
||||
|
||||
def test_move_returns_none_on_missing_or_nonstring_or_already_ref():
|
||||
data = {"env": {"API_KEY": "${API_KEY}", "N": 5}}
|
||||
assert c.move_value_to_env_ref(data, field="env", key="ABSENT") is None
|
||||
assert c.move_value_to_env_ref(data, field="env", key="N") is None # not a string
|
||||
assert c.move_value_to_env_ref(data, field="env", key="API_KEY") is None # already a ref
|
||||
assert c.move_value_to_env_ref({}, field="bogus") is None
|
||||
assert c.move_value_to_env_ref({"args": ["a"]}, field="args", index=9) is None
|
||||
|
||||
|
||||
def test_is_env_var_set():
|
||||
assert c.is_env_var_set("FOO", {"FOO": "x"}) is True
|
||||
assert c.is_env_var_set("FOO", {"FOO": ""}) is False
|
||||
assert c.is_env_var_set("FOO", {}) is False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Args secret indices + suggested var name (issue #83, args surface)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_secret_arg_indices_flags_token_and_flag_value():
|
||||
args = ["--port", "8080", "ghp_deadbeef", "--token", "sk-abc", "--flag=val"]
|
||||
idxs = c.secret_arg_indices(args)
|
||||
assert 2 in idxs # ghp_ token prefix
|
||||
assert 4 in idxs # value following --token
|
||||
assert 1 not in idxs # 8080
|
||||
assert 5 not in idxs # --flag=val inline pair
|
||||
|
||||
|
||||
def test_secret_arg_indices_flags_embedded_url_credentials():
|
||||
args = ["postgres://user:pass@host/db"]
|
||||
assert c.secret_arg_indices(args) == [0]
|
||||
|
||||
|
||||
def test_secret_arg_indices_excludes_existing_references():
|
||||
args = ["--token", "${GH_TOKEN}"]
|
||||
assert c.secret_arg_indices(args) == []
|
||||
|
||||
|
||||
def test_suggested_env_var_for_arg_uses_preceding_flag():
|
||||
args = ["--api-key", "sk-secret"]
|
||||
assert c.suggested_env_var_for_arg(args, 1) == "API_KEY"
|
||||
|
||||
|
||||
def test_suggested_env_var_for_arg_falls_back_when_no_flag():
|
||||
args = ["ghp_secret"]
|
||||
assert c.suggested_env_var_for_arg(args, 0) == "SECRET"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Referenced-variables readout + args->env relocation (issue #83, "both")
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_referenced_env_vars_dedupes_and_reports_status():
|
||||
data = {
|
||||
"command": "npx",
|
||||
"args": ["--token", "${GH_TOKEN}", "${GH_TOKEN}"],
|
||||
"env": {"API_KEY": "${API_KEY}", "REGION": "${REGION:-us-east-1}"},
|
||||
}
|
||||
usages = c.referenced_env_vars(data, environ={"GH_TOKEN": "x"})
|
||||
by = {u.name: u for u in usages}
|
||||
assert set(by) == {"GH_TOKEN", "API_KEY", "REGION"}
|
||||
# GH_TOKEN appears only in args, deduped to one entry, set in env -> resolved
|
||||
assert by["GH_TOKEN"].fields == ("args",)
|
||||
assert by["GH_TOKEN"].resolved is True
|
||||
# API_KEY not set, no default -> unresolved
|
||||
assert by["API_KEY"].resolved is False
|
||||
# REGION has a default -> resolved regardless of environment
|
||||
assert by["REGION"].has_default is True
|
||||
assert by["REGION"].resolved is True
|
||||
# sorted by name
|
||||
assert [u.name for u in usages] == sorted(u.name for u in usages)
|
||||
|
||||
|
||||
def test_referenced_env_vars_empty_when_no_refs():
|
||||
assert c.referenced_env_vars({"command": "npx", "args": ["-y", "pkg"]}) == []
|
||||
|
||||
|
||||
def test_move_arg_to_env_block_removes_flag_and_value():
|
||||
data = {"command": "x", "args": ["--api-key", "sk-secret", "run"], "env": {"KEEP": "1"}}
|
||||
out = c.move_arg_to_env_block(data, 1)
|
||||
assert out["args"] == ["run"] # flag + value both gone
|
||||
assert out["env"]["API_KEY"] == "sk-secret"
|
||||
assert out["env"]["KEEP"] == "1" # existing env preserved
|
||||
# input not mutated
|
||||
assert data["args"] == ["--api-key", "sk-secret", "run"]
|
||||
|
||||
|
||||
def test_move_arg_to_env_block_bare_positional_keeps_no_flag():
|
||||
data = {"command": "x", "args": ["ghp_secret", "serve"]}
|
||||
out = c.move_arg_to_env_block(data, 0, var_name="GITHUB_TOKEN")
|
||||
assert out["args"] == ["serve"]
|
||||
assert out["env"] == {"GITHUB_TOKEN": "ghp_secret"}
|
||||
|
||||
|
||||
def test_move_arg_to_env_block_none_on_bad_target():
|
||||
assert c.move_arg_to_env_block({"args": ["a"]}, 5) is None
|
||||
assert c.move_arg_to_env_block({"args": ["${REF}"]}, 0) is None # already a ref
|
||||
assert c.move_arg_to_env_block({}, 0) is None
|
||||
|
||||
Reference in New Issue
Block a user