Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c7d668e
feat(cli): add `scan --redirect` hosted-vendored-patch mode
mikolalysenko Jul 1, 2026
8fcd7b3
feat(vex): attest redirected patches with a (redirected) provenance m…
mikolalysenko Jul 1, 2026
7b23bc8
Merge remote-tracking branch 'origin/main' into feat/scan-redirect-mode
mikolalysenko Jul 1, 2026
b47b9d4
style: cargo fmt
mikolalysenko Jul 1, 2026
554b486
test(vex): real-install npm redirect capstone + docker gem/composer V…
mikolalysenko Jul 1, 2026
baedf28
fix(redirect,vex): fail-closed attestation, idempotent rewriters, sur…
mikolalysenko Jul 1, 2026
be23ae2
fix(ci): check the redirect golden fixtures out byte-exact on Windows
mikolalysenko Jul 1, 2026
e93f21c
chore: exclude test fixtures from Socket dependency scanning
mikolalysenko Jul 1, 2026
0c925d1
feat(cli): three-mode selector, maven hosted rewriter, nuget config f…
mikolalysenko Jul 2, 2026
a8dfb7e
feat(vendor): NuGet and Maven vendored backends with fragment-level r…
mikolalysenko Jul 2, 2026
1cfd02c
test: three-mode × ecosystem behavioral matrix
mikolalysenko Jul 2, 2026
a85f454
docs: three-mode README, CHANGELOG, CLI_CONTRACT + support matrix
mikolalysenko Jul 2, 2026
8399416
test(vendor): jsr is the unsupported-ecosystem exemplar now that nuge…
mikolalysenko Jul 2, 2026
56f3550
fix(cli): mode-conflict errors match the clap contract phrasing
mikolalysenko Jul 2, 2026
d9aa32f
docs: spell out Maven's checksum-failure fallback for hosted mode
mikolalysenko Jul 2, 2026
7ee80d7
refactor(core): factor bun text-lock grammar + share uri encode; gene…
mikolalysenko Jul 2, 2026
1db0d6d
feat(patches): Rush monorepo support — vendored refusal, scan invento…
mikolalysenko Jul 2, 2026
2716592
feat(core): yarn-berry + bun hosted registry-redirect rewriters + sha…
mikolalysenko Jul 2, 2026
20534a0
feat(cli): berry/bun redirect plumbing + bun.lockb auto-migration
mikolalysenko Jul 2, 2026
8154b18
feat(cli): scan --redirect discovers Rush pnpm locks + stale repo-sta…
mikolalysenko Jul 2, 2026
d6f736b
test(redirect,repair): pnpm hosted-lock legs + flavor repair matrix
mikolalysenko Jul 2, 2026
29bbb80
test(apply): agent-mode legs for bun, yarn-berry node-modules, rush farm
mikolalysenko Jul 2, 2026
58decbc
feat(redirect): fail-closed maven suffixing + trusted checksums
mikolalysenko Jul 2, 2026
bd317b8
test(redirect): real-install berry + bun hosted capstones
mikolalysenko Jul 2, 2026
dea45e6
test(docker): yarn berry 4.x agent + vendored e2e legs
mikolalysenko Jul 2, 2026
9063242
test(redirect): Rush hosted-mode redirect capstone (sim + gated real …
mikolalysenko Jul 2, 2026
bd76bcb
test(redirect): clippy cleanups in berry + rush redirect legs
mikolalysenko Jul 2, 2026
bf74543
docs: berry/bun + Rush hosted support, maven fail-closed, de-document…
mikolalysenko Jul 2, 2026
8f591de
style(core): satisfy clippy cloned_ref_to_slice_refs in rewriter tests
mikolalysenko Jul 2, 2026
14088c9
test(cli): fix windows + release CI failures in in-process suites
mikolalysenko Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test(apply): agent-mode legs for bun, yarn-berry node-modules, rush farm
Extend `in_process_alternate_installers.rs` with three real-layout
agent-mode apply legs:

- bun: real `bun install` (private cache) → apply patches the hoisted
  node_modules copy;
- yarn-berry node-modules linker: `.yarnrc.yml nodeLinker: node-modules`
  + corepack-dispatched `yarn@4.12.0`, install → apply (complements the
  PnP refusal test in `e2e_safety_yarn_pnp.rs`);
- rush pnpm symlink farm: a hand-built
  `common/temp/node_modules/.pnpm/<pkg>@<v>/…` store with per-project
  `apps/{a,b}/node_modules/<pkg>` symlinks and `rush.json` at the root;
  apply at the root patches the canonical `.pnpm` file once and the
  bytes are visible through BOTH symlinks — pinning that the crawl finds
  the package via the project symlinks even though `common/temp` is in
  SKIP_DIRS.

Assisted-by: Claude Code:claude-fable-5
  • Loading branch information
mikolalysenko committed Jul 2, 2026
commit 29bbb802ed126c56f1480faf5df82b1fb0eace3f
248 changes: 248 additions & 0 deletions crates/socket-patch-cli/tests/in_process_alternate_installers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,251 @@ gem 'colorize', '1.1.0'
assert_eq!(code, 0, "bundler-installed gem must be patchable");
assert_patched(&lib_file, &patched, &before_hash, &after_hash);
}

// ---------------------------------------------------------------------------
// bun install layout
// ---------------------------------------------------------------------------

/// bun installs a hoisted node_modules by default (like npm), so this exercises
/// that a bun-installed package is patched in place by agent-mode apply. Gated
/// on `bun` on PATH like the other real-installer legs; a failed fixture
/// install skips, but a missing file after a *successful* install is a hard
/// regression.
#[tokio::test]
#[serial]
async fn bun_install_then_apply_patches_file() {
if !has("bun") {
println!("SKIP: bun not on PATH");
return;
}

let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("package.json"),
r#"{ "name": "bun-test", "version": "0.0.0", "dependencies": { "ms": "2.1.3" } }"#,
)
.unwrap();

// Private cache so the fixture install never touches the user's bun cache.
let cache = tmp.path().join("bun-cache");
let status = Command::new("bun")
.args(["install", "--no-progress"])
.current_dir(tmp.path())
.env("BUN_INSTALL_CACHE_DIR", &cache)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("bun install");
if !status.status.success() {
println!(
"SKIP: bun install failed: {}",
String::from_utf8_lossy(&status.stderr)
);
return;
}

let ms_index = tmp.path().join("node_modules/ms/index.js");
assert!(
ms_index.exists(),
"bun install succeeded but node_modules/ms/index.js is missing at {ms_index:?}"
);

let original = std::fs::read(&ms_index).expect("read ms/index.js");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-BUN-MARKER\n");
let after_hash = git_sha256(&patched);

let socket = tmp.path().join(".socket");
write_manifest(&socket, "pkg:npm/ms@2.1.3", &before_hash, &after_hash);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();

let code = apply_run(default_apply(tmp.path())).await;
assert_eq!(
code, 0,
"apply must succeed against a bun-installed package"
);
assert_patched(&ms_index, &patched, &before_hash, &after_hash);
}

// ---------------------------------------------------------------------------
// yarn berry (4.x) node-modules linker
// ---------------------------------------------------------------------------

fn has_corepack_pm(pm: &str) -> bool {
Command::new("corepack")
.args([pm, "--version"])
.env("COREPACK_ENABLE_DOWNLOAD_PROMPT", "0")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}

/// yarn berry with the **node-modules** linker (`.yarnrc.yml` `nodeLinker:
/// node-modules` + `packageManager: yarn@4.12.0` so corepack dispatches berry)
/// installs a real hoisted `node_modules/ms`, which agent-mode apply must
/// patch in place. This complements `e2e_safety_yarn_pnp.rs` (which asserts
/// berry's PnP linker is REFUSED because packages live in `.yarn/cache` zips):
/// under the node-modules linker the on-disk layout is patchable.
#[tokio::test]
#[serial]
async fn yarn_berry_node_modules_linker_apply_patches_file() {
if !has_corepack_pm("yarn@4.12.0") {
println!("SKIP: corepack yarn@4.12.0 unavailable");
return;
}

let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("package.json"),
r#"{ "name": "berry-nm-test", "version": "0.0.0", "packageManager": "yarn@4.12.0", "dependencies": { "ms": "2.1.3" } }"#,
)
.unwrap();
std::fs::write(
tmp.path().join(".yarnrc.yml"),
"nodeLinker: node-modules\nenableGlobalCache: false\n",
)
.unwrap();

let global = tmp.path().join("yarn-global");
let status = Command::new("corepack")
.args(["yarn@4.12.0", "install"])
.current_dir(tmp.path())
.env("COREPACK_ENABLE_DOWNLOAD_PROMPT", "0")
.env("YARN_GLOBAL_FOLDER", &global)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("corepack yarn install");
if !status.status.success() {
println!(
"SKIP: yarn berry install failed: {}",
String::from_utf8_lossy(&status.stderr)
);
return;
}

// node-modules linker premise: ms is a real hoisted directory (NOT a PnP
// .yarn/cache zip). If berry ever changed the default layout under this
// linker we would not be exercising the in-place patch path and must know.
let ms_index = tmp.path().join("node_modules/ms/index.js");
assert!(
ms_index.exists(),
"yarn berry (node-modules linker) install succeeded but node_modules/ms/index.js \
is missing at {ms_index:?} — layout premise broken"
);

let original = std::fs::read(&ms_index).expect("read ms/index.js");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-BERRY-NM-MARKER\n");
let after_hash = git_sha256(&patched);

let socket = tmp.path().join(".socket");
write_manifest(&socket, "pkg:npm/ms@2.1.3", &before_hash, &after_hash);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();

let code = apply_run(default_apply(tmp.path())).await;
assert_eq!(
code, 0,
"apply must succeed against the yarn-berry node-modules layout"
);
assert_patched(&ms_index, &patched, &before_hash, &after_hash);
}

// ---------------------------------------------------------------------------
// Rush pnpm symlink farm (hand-built, no real installer)
// ---------------------------------------------------------------------------

/// Hand-build the exact layout Rush + pnpm produce: a single canonical package
/// under `common/temp/node_modules/.pnpm/<pkg>@<v>/node_modules/<pkg>/` (real
/// files) plus per-project symlinks `apps/{a,b}/node_modules/<pkg>` pointing
/// into it, with `rush.json` at the repo root. Running agent-mode apply at the
/// repo root must patch the canonical file ONCE and have the patched bytes
/// visible through BOTH project symlinks.
///
/// This pins the discovery mechanism: `common/temp` is in the crawler's
/// SKIP_DIRS (`temp`), so the canonical `.pnpm` store is NOT walked directly —
/// the package is found via the `apps/*/node_modules` symlinks (which the
/// crawler follows into the farm), exactly as it must be for a real Rush repo.
#[cfg(unix)]
#[tokio::test]
#[serial]
async fn rush_pnpm_symlink_farm_apply_patches_through_both_projects() {
use std::os::unix::fs::symlink;

let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
// rush.json at the root marks this a Rush repo.
std::fs::write(root.join("rush.json"), r#"{ "rushVersion": "5.100.0" }"#).unwrap();

// The canonical package lives in the pnpm virtual store under common/temp.
let canonical_dir = root.join("common/temp/node_modules/.pnpm/ms@2.1.3/node_modules/ms");
std::fs::create_dir_all(&canonical_dir).unwrap();
std::fs::write(
canonical_dir.join("package.json"),
r#"{ "name": "ms", "version": "2.1.3" }"#,
)
.unwrap();
let original = b"module.exports = function ms() {}\n".to_vec();
std::fs::write(canonical_dir.join("index.js"), &original).unwrap();

// Two Rush projects, each with a node_modules/ms symlink INTO the farm.
for app in ["a", "b"] {
let nm = root.join(format!("apps/{app}/node_modules"));
std::fs::create_dir_all(&nm).unwrap();
std::fs::write(
root.join(format!("apps/{app}/package.json")),
format!(r#"{{ "name": "app-{app}", "version": "1.0.0", "dependencies": {{ "ms": "2.1.3" }} }}"#),
)
.unwrap();
symlink(&canonical_dir, nm.join("ms")).unwrap();
}

let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-RUSH-FARM-MARKER\n");
let after_hash = git_sha256(&patched);

let socket = root.join(".socket");
write_manifest(&socket, "pkg:npm/ms@2.1.3", &before_hash, &after_hash);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();

let code = apply_run(default_apply(root)).await;
assert_eq!(
code, 0,
"apply must succeed against the Rush pnpm symlink farm (crawl found the package \
via the apps/*/node_modules symlinks despite common/temp being skipped)"
);

// The canonical file under .pnpm is the one that must carry the patched
// bytes — apply followed the symlink into the store rather than shadowing.
assert_patched(
&canonical_dir.join("index.js"),
&patched,
&before_hash,
&after_hash,
);
// Both project symlinks resolve to the patched canonical file.
for app in ["a", "b"] {
let via = root.join(format!("apps/{app}/node_modules/ms/index.js"));
assert_eq!(
std::fs::read(&via).unwrap(),
patched,
"the patched bytes must be visible through apps/{app}'s symlink at {via:?}"
);
let real = std::fs::canonicalize(&via).expect("canonicalize symlink");
assert!(
real.components().any(|c| c.as_os_str() == ".pnpm"),
"apps/{app}'s symlink must resolve into the .pnpm farm, not a shadow copy: {real:?}"
);
}
}