feat(#92): detect unpinned npx specs, resolve version, one-click pin, drift
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 9s

`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went
v1 → v2 and the tool set changed under a running agent, mid-task, with no warning.
This detects that and turns it into a comprehensible pin/upgrade prompt.

Core (pure, no network — reuses parse_version / is_newer_version / the catalog
spec parsers; the resolved-version lookup is fully injectable for tests):
- server_package_spec / server_package_name — the npm spec an npx-style server runs.
- is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact
  numeric version is pinned.
- resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins),
  degrades to None cleanly. NO network. find/read injectable.
- pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's
  (new_data, note) contract). No-op when already pinned / bad version / not npx.
- version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version.
- version_status — the badge's high-level dict (unpinned / pinned_version /
  resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an
  explicit resolved=None ("unknown") vs. omitting it to do the local lookup.

GUI: a version badge row under the dependency status (mirrors that surface) with a
one-click "Pin to <version>" button, shown only for npx servers; a drift note when
a pinned version has been overtaken. Smoke-tested headlessly.

pytest green (520 passed), ruff + format clean. Closes #92. Part of epic #94.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cowork Supervisor
2026-08-12 03:04:53 -04:00
co-authored by Claude Opus 4.8
parent e087107710
commit 2cd8e0fb3b
3 changed files with 350 additions and 0 deletions
+106
View File
@@ -3297,6 +3297,112 @@ def test_drift_warning_quiet_for_other_values_and_unknown_pkg():
assert c.drift_warnings({"command": "npx", "args": ["other", "--maxChars=none"]}) == []
# --------------------------------------------------------------------------- #
# Version resolve + pin + drift (issue #92)
# --------------------------------------------------------------------------- #
def test_server_package_spec_and_name():
assert c.server_package_spec({"command": "npx", "args": ["-y", "ssh-mcp"]}) == "ssh-mcp"
assert c.server_package_spec({"command": "npx", "args": ["ssh-mcp@2.1.0"]}) == "ssh-mcp@2.1.0"
assert c.server_package_name({"command": "npx", "args": ["ssh-mcp@2.1.0"]}) == "ssh-mcp"
# A direct binary launch has no run-time spec to pin.
assert c.server_package_spec({"command": "ssh-mcp", "args": []}) is None
assert c.server_package_spec({"url": "https://x"}) is None
def test_is_unpinned_spec():
assert c.is_unpinned_spec({"command": "npx", "args": ["-y", "ssh-mcp"]}) is True
assert c.is_unpinned_spec({"command": "npx", "args": ["ssh-mcp@latest"]}) is True
assert c.is_unpinned_spec({"command": "npx", "args": ["ssh-mcp@next"]}) is True
assert c.is_unpinned_spec({"command": "npx", "args": ["ssh-mcp@2.1.0"]}) is False
assert c.is_unpinned_spec({"command": "ssh-mcp", "args": []}) is False
def test_parse_package_json_version():
assert c.parse_package_json_version('{"name":"ssh-mcp","version":"2.1.0"}') == "2.1.0"
assert c.parse_package_json_version('{"name":"x"}') is None
assert c.parse_package_json_version("not json") is None
assert c.parse_package_json_version('{"version":""}') is None
def test_resolved_npx_version_reads_cache_highest_wins():
# Two cache entries for ssh-mcp; the highest version wins. No real filesystem.
files = {
"/h/.npm/_npx/aaa/node_modules/ssh-mcp/package.json": '{"version":"1.9.0"}',
"/h/.npm/_npx/bbb/node_modules/ssh-mcp/package.json": '{"version":"2.1.0"}',
}
got = c.resolved_npx_version(
"ssh-mcp",
home="/h",
find=lambda pat: list(files),
read=lambda p: files[p],
)
assert got == "2.1.0"
def test_resolved_npx_version_unknown_degrades_to_none():
assert (
c.resolved_npx_version("ssh-mcp", home="/h", find=lambda pat: [], read=lambda p: "") is None
)
assert c.resolved_npx_version("", home="/h") is None
def test_pin_spec_transform():
data = {"command": "npx", "args": ["-y", "ssh-mcp", "--host=h"]}
new, note = c.pin_spec_transform(data, "2.1.0")
assert new["args"] == ["-y", "ssh-mcp@2.1.0", "--host=h"]
assert note and "2.1.0" in note
# Already pinned to that exact version -> no-op.
again, note2 = c.pin_spec_transform(new, "2.1.0")
assert again == new
assert note2 is None
# Bad / empty version -> no-op.
assert c.pin_spec_transform(data, "")[1] is None
assert c.pin_spec_transform(data, "latest")[1] is None
# Not an npx server -> no-op.
assert c.pin_spec_transform({"command": "ssh-mcp", "args": []}, "2.1.0")[1] is None
def test_version_drift_note():
assert c.version_drift_note("2.1.0", "3.0.0") == "moved 2.1.0 → 3.0.0 since you pinned"
assert c.version_drift_note("3.0.0", "3.0.0") is None
assert c.version_drift_note("3.0.0", "2.1.0") is None # never a backwards "drift"
assert c.version_drift_note(None, "3.0.0") is None
assert c.version_drift_note("2.1.0", None) is None
# Numeric, not lexical: 2 < 10.
assert c.version_drift_note("2.0.0", "10.0.0") is not None
def test_version_status_unpinned_offers_pin():
st = c.version_status({"command": "npx", "args": ["-y", "ssh-mcp"]}, resolved="2.1.0")
assert st["package"] == "ssh-mcp"
assert st["unpinned"] is True
assert st["pinned_version"] is None
assert st["resolved_version"] == "2.1.0"
assert st["can_pin"] is True
assert st["drift"] is None
def test_version_status_pinned_reports_drift():
st = c.version_status({"command": "npx", "args": ["ssh-mcp@2.1.0"]}, resolved="3.0.0")
assert st["unpinned"] is False
assert st["pinned_version"] == "2.1.0"
assert st["can_pin"] is False # already pinned
assert st["drift"] == "moved 2.1.0 → 3.0.0 since you pinned"
def test_version_status_unknown_resolved_cannot_pin():
st = c.version_status({"command": "npx", "args": ["-y", "ssh-mcp"]}, resolved=None)
assert st["unpinned"] is True
assert st["resolved_version"] is None
assert st["can_pin"] is False # nothing to pin TO
assert st["drift"] is None
def test_version_status_none_for_non_npx():
assert c.version_status({"command": "ssh-mcp", "args": []}) is None
assert c.version_status({"url": "https://x"}) is None
# --------------------------------------------------------------------------- #
# Move to environment variable (issue #83)
# --------------------------------------------------------------------------- #