diff --git a/audit.py b/audit.py index cb34196..c12f48a 100755 --- a/audit.py +++ b/audit.py @@ -36,7 +36,7 @@ from cli_audit.logging_config import setup_logging # noqa: E402 # Split file support (Phase 2.1) from cli_audit.upstream_cache import ( # noqa: E402 - UpstreamVersion, UpstreamCache, + UpstreamVersion, load_upstream_cache, write_upstream_cache, get_upstream_cache_path, update_cached_upstream, ) @@ -1025,9 +1025,12 @@ def cmd_update_baseline(args: argparse.Namespace) -> int: print(f"# Collecting upstream versions for {total} tools...", file=sys.stderr) - # Collect upstream versions only - upstream_cache = UpstreamCache() + # Collect upstream versions only. Merge into the existing baseline so a + # tool-scoped run (or a transient collection failure) never drops the + # entries that were not collected this run (issue #125). + upstream_cache = load_upstream_cache() completed = 0 + collected = 0 with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, total)) as executor: future_to_tool = {} @@ -1048,6 +1051,7 @@ def cmd_update_baseline(args: argparse.Namespace) -> int: ) upstream_cache.versions[tool.name] = version completed += 1 + collected += 1 display = latest_num or latest_tag or "n/a" print(f"# [{completed}/{total}] {tool.name}: {display}", file=sys.stderr) @@ -1059,7 +1063,7 @@ def cmd_update_baseline(args: argparse.Namespace) -> int: write_upstream_cache(upstream_cache) print("", file=sys.stderr) print(f"✓ Upstream baseline updated: {get_upstream_cache_path()}", file=sys.stderr) - print(f"✓ Collected {len(upstream_cache.versions)} versions", file=sys.stderr) + print(f"✓ Collected {collected}/{total} versions", file=sys.stderr) # Report rate limit rate_limit = get_github_rate_limit() diff --git a/tests/test_update_fixes.py b/tests/test_update_fixes.py index 8e0e294..31a27d3 100644 --- a/tests/test_update_fixes.py +++ b/tests/test_update_fixes.py @@ -1260,3 +1260,57 @@ def test_refreshes_installed_but_preserves_latest(self, tmp_path): assert entry["installed_version"] not in ("", "0.0.1"), "installed must be refreshed" # directional status: real git (e.g. 2.x) < 999.0.0 -> OUTDATED assert entry["status"] == "OUTDATED" + + +@skip_on_windows +class TestUpdateBaselineMerges: + """A tool-scoped `--update-baseline ` must MERGE the collected entry + into the existing committed baseline, never replace the whole file with only + the requested tool (issue #125: `--update-baseline vault` dropped every other + entry). Failed collections must likewise leave the existing entry intact.""" + + def _seed_baseline(self, path): + path.write_text(json.dumps({ + "__meta__": {"baseline_updated_at": "2026-01-01T00:00:00Z", + "schema_version": 2, "source": "test"}, + "versions": { + "keepme": {"latest_tag": "v1.0.0", "latest_version": "1.0.0", + "latest_url": "", "tool_url": "", "upstream_method": "gh"}, + "ripgrep": {"latest_tag": "v0.0.1", "latest_version": "0.0.1", + "latest_url": "", "tool_url": "", "upstream_method": "gh"}, + }, + })) + + def test_single_tool_update_preserves_other_entries(self, tmp_path, monkeypatch): + import argparse + import audit + baseline = tmp_path / "upstream_versions.json" + self._seed_baseline(baseline) + monkeypatch.setenv("CLI_AUDIT_UPSTREAM_FILE", str(baseline)) + + with patch.object(audit, "collect_latest_version", return_value=("v9.9.9", "9.9.9")), \ + patch.object(audit, "get_github_rate_limit", return_value=None): + rc = audit.cmd_update_baseline(argparse.Namespace(tools=["ripgrep"])) + + assert rc == 0 + data = json.loads(baseline.read_text()) + assert data["versions"]["ripgrep"]["latest_version"] == "9.9.9" + assert "keepme" in data["versions"], "unrelated entries must survive a tool-scoped run" + assert data["versions"]["keepme"]["latest_version"] == "1.0.0" + + def test_failed_collection_keeps_existing_entry(self, tmp_path, monkeypatch): + import argparse + import audit + baseline = tmp_path / "upstream_versions.json" + self._seed_baseline(baseline) + monkeypatch.setenv("CLI_AUDIT_UPSTREAM_FILE", str(baseline)) + + with patch.object(audit, "collect_latest_version", side_effect=RuntimeError("network down")), \ + patch.object(audit, "get_github_rate_limit", return_value=None): + rc = audit.cmd_update_baseline(argparse.Namespace(tools=["ripgrep"])) + + assert rc == 0 + data = json.loads(baseline.read_text()) + # transient failure must not delete the committed entry + assert data["versions"]["ripgrep"]["latest_version"] == "0.0.1" + assert "keepme" in data["versions"]