Skip to content

Commit cb96d0d

Browse files
committed
chore(crawlers): drop dead NpmPkgManager::as_tag + extend coverage
NpmPkgManager::as_tag() and its corresponding test were dead — apply.rs matches on the enum variants directly (NpmPkgManager::YarnBerryPnP / ::Pnpm) and the struct never derives Serialize, so the stringified tag was unreachable from any caller. While here, extract `parse_bun_bin_output` from `get_bun_global_prefix` so the path-derivation half of bun discovery is unit-testable without shelling out to a real `bun` binary, and add integration tests covering: * cargo: TOML parser stops at next section / ignores pre-package lines, Default impl, CARGO_HOME unset → $HOME/.cargo fallback * npm: parse_bun_bin_output happy path, empty stdout, root-only path Assisted-by: Claude Code:claude-opus-4-7
1 parent dc36eab commit cb96d0d

4 files changed

Lines changed: 113 additions & 24 deletions

File tree

crates/socket-patch-core/src/crawlers/npm_crawler.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,18 @@ pub fn get_bun_global_prefix() -> Option<String> {
155155
if !output.status.success() {
156156
return None;
157157
}
158+
parse_bun_bin_output(&String::from_utf8_lossy(&output.stdout))
159+
}
158160

159-
let bin_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
161+
/// Pure parser for `bun pm bin -g` stdout. Extracted so the
162+
/// derive-the-global-node_modules-path logic is unit-testable
163+
/// without shelling out.
164+
///
165+
/// Given output like `"/Users/foo/.bun/bin\n"` returns
166+
/// `Some("/Users/foo/.bun/install/global/node_modules")`. Returns
167+
/// `None` on empty input or a root-only path with no parent.
168+
pub fn parse_bun_bin_output(stdout: &str) -> Option<String> {
169+
let bin_path = stdout.trim().to_string();
160170
if bin_path.is_empty() {
161171
return None;
162172
}

crates/socket-patch-core/src/crawlers/pkg_managers.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,6 @@ pub enum NpmPkgManager {
4444
Unknown,
4545
}
4646

47-
impl NpmPkgManager {
48-
/// Short lowercase tag, suitable for JSON output.
49-
pub fn as_tag(&self) -> &'static str {
50-
match self {
51-
NpmPkgManager::Npm => "npm",
52-
NpmPkgManager::Pnpm => "pnpm",
53-
NpmPkgManager::YarnClassic => "yarn-classic",
54-
NpmPkgManager::YarnBerryPnP => "yarn-berry-pnp",
55-
NpmPkgManager::Unknown => "unknown",
56-
}
57-
}
58-
}
59-
6047
/// Detect the package manager that produced the layout under
6148
/// `project_root`. Inspection is purely path-based — no shell-outs,
6249
/// no parsing — so the detector is fast and side-effect-free.
@@ -173,13 +160,4 @@ mod tests {
173160
);
174161
}
175162

176-
#[test]
177-
fn as_tag_values() {
178-
// Pin the tag strings — they're part of the JSON envelope contract.
179-
assert_eq!(NpmPkgManager::Npm.as_tag(), "npm");
180-
assert_eq!(NpmPkgManager::Pnpm.as_tag(), "pnpm");
181-
assert_eq!(NpmPkgManager::YarnClassic.as_tag(), "yarn-classic");
182-
assert_eq!(NpmPkgManager::YarnBerryPnP.as_tag(), "yarn-berry-pnp");
183-
assert_eq!(NpmPkgManager::Unknown.as_tag(), "unknown");
184-
}
185163
}

crates/socket-patch-core/tests/crawler_cargo_e2e.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,81 @@ fn parse_cargo_toml_malformed_returns_none() {
7070
assert_eq!(parse_cargo_toml_name_version(toml), None);
7171
}
7272

73+
/// Parser must stop scanning when it leaves the `[package]` table.
74+
/// A `name =` or `version =` line under a later table must NOT be
75+
/// picked up. Covers the "left package section" early-break arm
76+
/// (cargo_crawler.rs:34-36).
77+
#[test]
78+
fn parse_cargo_toml_stops_at_next_section() {
79+
let toml = "[package]\nname = \"foo\"\nversion = \"1.0.0\"\n\n[dependencies]\nname = \"bar\"\n";
80+
assert_eq!(
81+
parse_cargo_toml_name_version(toml),
82+
Some(("foo".to_string(), "1.0.0".to_string()))
83+
);
84+
}
85+
86+
/// Parser must ignore key=value lines that appear BEFORE [package]
87+
/// (e.g. inside an earlier [profile.release] table).
88+
#[test]
89+
fn parse_cargo_toml_ignores_lines_before_package_section() {
90+
let toml = "[profile.release]\nname = \"wrong\"\n\n[package]\nname = \"foo\"\nversion = \"1.0.0\"\n";
91+
assert_eq!(
92+
parse_cargo_toml_name_version(toml),
93+
Some(("foo".to_string(), "1.0.0".to_string()))
94+
);
95+
}
96+
97+
/// CargoCrawler's `Default` impl forwards to `new`. Exercise both
98+
/// for symmetry.
99+
#[test]
100+
fn cargo_crawler_default_and_new_construct_cleanly() {
101+
let _a = CargoCrawler::default();
102+
let _b = CargoCrawler::new();
103+
}
104+
105+
/// `cargo_home` fallback to `$HOME/.cargo` when CARGO_HOME is unset.
106+
/// Exercised via `get_crate_source_paths(global=true)` which calls
107+
/// `Self::get_registry_src_paths` → `cargo_home` internally.
108+
#[tokio::test]
109+
#[serial_test::serial]
110+
async fn cargo_home_fallback_to_home_dot_cargo() {
111+
let tmp = tempfile::tempdir().unwrap();
112+
// Stage a fake registry tree at $HOME/.cargo/registry/src/.
113+
let stamp_dir = tmp
114+
.path()
115+
.join(".cargo")
116+
.join("registry")
117+
.join("src")
118+
.join("index.crates.io-1949cf8c6b5b557f");
119+
tokio::fs::create_dir_all(&stamp_dir).await.unwrap();
120+
121+
let prev_cargo = std::env::var("CARGO_HOME").ok();
122+
let prev_home = std::env::var("HOME").ok();
123+
std::env::remove_var("CARGO_HOME");
124+
std::env::set_var("HOME", tmp.path());
125+
126+
let crawler = CargoCrawler;
127+
let opts = CrawlerOptions {
128+
cwd: tmp.path().to_path_buf(),
129+
global: true,
130+
global_prefix: None,
131+
batch_size: 100,
132+
};
133+
let paths = crawler.get_crate_source_paths(&opts).await.unwrap();
134+
135+
if let Some(v) = prev_cargo {
136+
std::env::set_var("CARGO_HOME", v);
137+
}
138+
if let Some(v) = prev_home {
139+
std::env::set_var("HOME", v);
140+
}
141+
142+
assert!(
143+
paths.iter().any(|p| p == &stamp_dir),
144+
"HOME/.cargo fallback registry must be discovered; got {paths:?}"
145+
);
146+
}
147+
73148
// ── find_by_purls ──────────────────────────────────────────────
74149

75150
#[tokio::test]

crates/socket-patch-core/tests/crawler_npm_e2e.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::path::Path;
77

88
use socket_patch_core::crawlers::npm_crawler::{
9-
build_npm_purl, parse_package_name, read_package_json,
9+
build_npm_purl, parse_bun_bin_output, parse_package_name, read_package_json,
1010
};
1111
use socket_patch_core::crawlers::types::CrawlerOptions;
1212
use socket_patch_core::crawlers::NpmCrawler;
@@ -119,6 +119,32 @@ async fn read_package_json_missing_version_returns_none() {
119119
assert_eq!(result, None);
120120
}
121121

122+
// ── parse_bun_bin_output ───────────────────────────────────────
123+
124+
/// Bun's global node_modules lives at `<bun-root>/install/global/node_modules`
125+
/// — the parser strips the trailing `bin` segment and joins the well-known
126+
/// suffix.
127+
#[test]
128+
fn parse_bun_bin_output_well_formed_unix() {
129+
let parsed = parse_bun_bin_output("/home/foo/.bun/bin\n");
130+
assert_eq!(
131+
parsed.as_deref(),
132+
Some("/home/foo/.bun/install/global/node_modules")
133+
);
134+
}
135+
136+
#[test]
137+
fn parse_bun_bin_output_empty_returns_none() {
138+
assert_eq!(parse_bun_bin_output(""), None);
139+
assert_eq!(parse_bun_bin_output(" \n "), None);
140+
}
141+
142+
/// Root-only path has no parent — must yield None instead of panicking.
143+
#[test]
144+
fn parse_bun_bin_output_root_path_returns_none() {
145+
assert_eq!(parse_bun_bin_output("/"), None);
146+
}
147+
122148
// ── find_by_purls ──────────────────────────────────────────────
123149

124150
#[tokio::test]

0 commit comments

Comments
 (0)