feat(#90): promote FLAG_ENV_MIGRATIONS into a ServerSpec spine
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 25s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 6s
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 25s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 6s
Introduce the server-package axis (orthogonal to ClientSpec): a frozen ServerSpec dataclass + SERVER_SPECS registry that the sidecar (#91), version-pin (#92) and permission (#93) work all hang off. - ServerSpec carries env_flags / removed_flags / drift_flags / sidecar_paths / sidecar_doc_path / schema. FLAG_ENV_MIGRATIONS is now a derived view of the registry, so every existing reader and the migration functions keep the exact shape #89 shipped — the migration LOGIC is unchanged, only the DATA grew. - resolve_server_spec(data) is the ServerSpec entry point; detect_migratable_package is a thin name-only wrapper over it (behaviour identical). - Add the verified #4 follow-ups: --sudoPassword AND --suPassword now auto-migrate to SSH_MCP_SUDO_PASSWORD (two flags → one var; existing no-clobber handles it). --disableSudo stays warn-only (sudo is now a role/policy, no env replacement). - Add drift_warnings(): --maxChars=none changed meaning (v1 silently capped at 5000 chars). Warn-only, no auto-fix; matches inline and separate arg forms. - Seed ssh-mcp's verified per-platform sidecar TOML paths and zod enums (auth/approvalMode/role/port) as data for later issues. Pure core + tests, no GUI. One #89 test (sudoPassword now migratable) updated to reflect the intended data growth, with a comment citing the verified facts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
436524bf00
commit
e087107710
+92
-3
@@ -3169,13 +3169,38 @@ def test_removed_flag_warnings_covers_migratable_and_manual():
|
||||
assert "--password" in text
|
||||
assert "sudoPassword" in text
|
||||
assert "disableSudo" in text
|
||||
# migrate only touches the confirmed --password mapping
|
||||
# #90: --password AND --sudoPassword are now both auto-migratable (the
|
||||
# verified ssh-mcp facts map sudo/su → SSH_MCP_SUDO_PASSWORD). The migration
|
||||
# LOGIC is unchanged — only the registry data grew — so both move into env.
|
||||
# --disableSudo has no env replacement (sudo is now a role/policy) → warn-only.
|
||||
new, _ = c.migrate_removed_flags(data)
|
||||
assert new["env"] == {"SSH_MCP_PASSWORD": "p"}
|
||||
assert "--sudoPassword=s" in new["args"]
|
||||
assert new["env"] == {"SSH_MCP_PASSWORD": "p", "SSH_MCP_SUDO_PASSWORD": "s"}
|
||||
assert "--sudoPassword=s" not in new["args"]
|
||||
assert "--disableSudo" in new["args"]
|
||||
|
||||
|
||||
def test_migrate_su_password_maps_to_sudo_env():
|
||||
# --suPassword is the second flag that maps to the same SSH_MCP_SUDO_PASSWORD var.
|
||||
data = {"command": "ssh-mcp", "args": ["--suPassword", "rootpw"]}
|
||||
new, notes = c.migrate_removed_flags(data)
|
||||
assert new["env"] == {"SSH_MCP_SUDO_PASSWORD": "rootpw"}
|
||||
assert "args" not in new
|
||||
assert notes
|
||||
|
||||
|
||||
def test_migrate_sudo_and_su_two_flags_one_var_no_clobber():
|
||||
# Both sudo flags map to one var; the no-clobber path keeps the first, drops
|
||||
# the second (different value) with a note rather than silently overwriting.
|
||||
data = {
|
||||
"command": "ssh-mcp",
|
||||
"args": ["--sudoPassword=first", "--suPassword=second"],
|
||||
}
|
||||
new, notes = c.migrate_removed_flags(data)
|
||||
assert new["env"] == {"SSH_MCP_SUDO_PASSWORD": "first"}
|
||||
assert "args" not in new
|
||||
assert any("already set" in n for n in notes)
|
||||
|
||||
|
||||
def test_removed_flag_warnings_empty_when_clean():
|
||||
assert c.removed_flag_warnings({"command": "ssh-mcp", "args": ["--host=h"]}) == []
|
||||
assert c.removed_flag_warning({"command": "ssh-mcp", "args": ["--host=h"]}) is None
|
||||
@@ -3208,6 +3233,70 @@ def test_ssh_membermatters_end_to_end():
|
||||
assert notes
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ServerSpec spine (issue #90)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_server_spec_registry_seeds_ssh_mcp():
|
||||
spec = c.SERVER_SPECS["ssh-mcp"]
|
||||
assert spec.package == "ssh-mcp"
|
||||
# sudo/su both map to the one sudo env var (verified facts).
|
||||
assert spec.env_flags["--sudoPassword"] == "SSH_MCP_SUDO_PASSWORD"
|
||||
assert spec.env_flags["--suPassword"] == "SSH_MCP_SUDO_PASSWORD"
|
||||
assert spec.env_flags["--password"] == "SSH_MCP_PASSWORD"
|
||||
# disableSudo stays warn-only (no env replacement).
|
||||
assert "--disableSudo" in spec.removed_flags
|
||||
assert "--disableSudo" not in spec.env_flags
|
||||
# Verified sidecar paths seeded per platform (README path differs → doc_path).
|
||||
assert "ssh-mcp/config.toml" in spec.sidecar_paths["darwin"]
|
||||
assert "Application Support" in spec.sidecar_paths["darwin"]
|
||||
assert "APPDATA" in spec.sidecar_paths["win32"]
|
||||
assert spec.sidecar_doc_path == "~/.config/ssh-mcp/config.toml"
|
||||
# zod enums seeded as data (pick-lists later — #7).
|
||||
assert spec.schema["auth"] == ["agent", "key", "password", "keychain"]
|
||||
assert spec.schema["approvalMode"] == ["auto", "ask-destructive", "ask-all", "deny"]
|
||||
assert spec.schema["role"] == ["viewer", "operator", "admin"]
|
||||
assert spec.schema["port"] == {"min": 1, "max": 65535}
|
||||
|
||||
|
||||
def test_flag_env_migrations_is_derived_from_server_specs():
|
||||
# The legacy constant is now a derived view; it must mirror the registry.
|
||||
assert set(c.FLAG_ENV_MIGRATIONS) == set(c.SERVER_SPECS)
|
||||
assert c.FLAG_ENV_MIGRATIONS["ssh-mcp"]["env"] == c.SERVER_SPECS["ssh-mcp"].env_flags
|
||||
assert c.FLAG_ENV_MIGRATIONS["ssh-mcp"]["removed"] == c.SERVER_SPECS["ssh-mcp"].removed_flags
|
||||
|
||||
|
||||
def test_resolve_server_spec_matches_detect_migratable_package():
|
||||
for data in (
|
||||
{"command": "npx", "args": ["-y", "ssh-mcp"]},
|
||||
{"command": "ssh-mcp", "args": []},
|
||||
{"command": "/usr/local/bin/ssh-mcp", "args": []},
|
||||
{"command": "npx", "args": ["some-other"]},
|
||||
{},
|
||||
{"url": "https://x"},
|
||||
):
|
||||
spec = c.resolve_server_spec(data)
|
||||
pkg = c.detect_migratable_package(data)
|
||||
assert (spec.package if spec else None) == pkg
|
||||
|
||||
|
||||
def test_drift_warning_maxchars_none_inline_and_separate():
|
||||
inline = {"command": "ssh-mcp", "args": ["--maxChars=none"]}
|
||||
separate = {"command": "ssh-mcp", "args": ["--maxChars", "none"]}
|
||||
for data in (inline, separate):
|
||||
warnings = c.drift_warnings(data)
|
||||
assert len(warnings) == 1
|
||||
assert "--maxChars=none" in warnings[0]
|
||||
assert "5000" in warnings[0]
|
||||
|
||||
|
||||
def test_drift_warning_quiet_for_other_values_and_unknown_pkg():
|
||||
# A real numeric cap is fine; only "none" drifts.
|
||||
assert c.drift_warnings({"command": "ssh-mcp", "args": ["--maxChars=8000"]}) == []
|
||||
assert c.drift_warnings({"command": "ssh-mcp", "args": ["--host=h"]}) == []
|
||||
# Unknown package: nothing to say.
|
||||
assert c.drift_warnings({"command": "npx", "args": ["other", "--maxChars=none"]}) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Move to environment variable (issue #83)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user