Skip to content

Commit ea09714

Browse files
Decide which lockfile governs installs in one table (#1044)
* Add one table for which lockfile governs installs formats/governing_locks.rs holds the npm-family precedence (vlt > bun > pnpm > yarn > npm, as wiring families) and the PyPI tool-lock precedence (uv > poetry > pdm > Pipfile), with presence-only queries over both. It is pure, so the disk, snapshot and in-memory views all feed it. Audit B31 (section 3.A). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Route npm-family lock precedence through the governing table The vendored npm router is now written once over ProjectView (detect_npm_lock_flavor_in); the hand-copied in-memory router in lock_inventory/view.rs is deleted. The pnpm-PnP layout check runs over the view too (pnpm_pnp_layout_in, with a new ProjectView::is_dir). The inventory's migration-leftover fallback, hosted vlt's sibling list, the hosted vlt preflight inputs and the hosted npm rewriter's another-lock-owns-it check all ask the table instead of spelling the lock list again. Visible differences: the in-memory router now gets the pnpm-PnP carve-out, multiple-lockfile warnings (discarded by its only caller) and the disk refusal wording; vendor_multiple_lockfiles warnings and the redirect_vlt_sibling_lockfiles file list follow precedence order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Route PyPI tool-lock precedence through the governing table detect_pypi_flavor, the hosted pdm_drives gate and the agent-mode PDM crawler's pdm_drives_project each spelled the uv > poetry > pdm > Pipfile order by hand. They now ask PYPI_TOOL_LOCKS. No behavior change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Warn when requirements.txt beside the governing PyPI lock stays unpatched Vendored PyPI wires one lock. A root requirements.txt exported beside the winning tool lock (pipenv requirements, uv export) never counted as a losing install source, so vendor left it unpatched without a word, vendor --check passed, and a hosted-to-vendored takeover silently restored its hosted pin to upstream (#612). When the root requirements.txt names the package being vendored, it is now listed among the losers of the documented pypi_multiple_lockfiles warning. The takeover runs the same router after its restore, so it now says so too. Keeping the hosted pin in the unwired file is a separate mixed-mode policy question and is not decided here. Refs #612 (audit B31). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Route the hosted lock-root lists through the governing table The hosted governing-root check kept its own hand lists of npm-family locks: OWN_LOCKS (chained with NPM_LOCKS and VLT_LOCK) for "the project is its own lock root", and WORKSPACE_ROOT_LOCKS for the npm/yarn/Bun workspace-root check. A lock spelling added to NpmLockFamily::files() would not have reached either. Derive both from the table: npm_lock_files() plus pnpm's pre-v3 shrinkwrap.yaml, and the Npm, Yarn and Bun families' files(). The workspace refusal still names the locks npm, yarn, Bun; within the npm family it now lists npm-shrinkwrap.json before package-lock.json (the family's own preference order) when both are present. Also spell requirements.txt through PYPI_REQUIREMENTS in the python-locks branch of detect_pypi_flavor, so the function names the file one way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Test the pnpm-PnP memory route and the #612 takeover warning - view.rs: the in-memory npm router now shares the pnpm-PnP carve-out with disk; pin it for both store markers (.modules.yaml and a .pnpm/ directory), and that a yarn.lock beside them keeps the yarn berry refusal. - mode_migration_pypi: a Pipenv project with an exported requirements.txt is hosted (both files pinned), then `vendor` takes it over. The takeover wires Pipfile.lock and must name requirements.txt in pypi_multiple_lockfiles as still installing the UNPATCHED bytes. Fails with the #612 requirements probe disabled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Name the hosted governing-root lists in the governing table's inventory Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent c4e65bc commit ea09714

16 files changed

Lines changed: 790 additions & 437 deletions

File tree

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1273,7 +1273,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
12731273
| `wiring_conflict` | `skipped` | vex (manifest-less): the lockfiles wire one package to two or more different patches (e.g. a stale sibling lock); which one the build installs is undecidable, so none is attested. |
12741274
| `hash_mismatch` / `not_applied` / `file_not_found` / `package_not_found` / `no_files` / `vendor_*` | `skipped` | vex: verification omissions — the installed copy (agent / hosted) or the committed artifact (`vendor_hash_mismatch`, `vendor_artifact_missing`, `vendor_artifact_unreadable`, `vendor_inventory_mismatch`, `vendor_uuid_mismatch`, `vendor_path_unsafe`) does not carry the patched bytes, or nothing is installed. `vendor_manifest_unverifiable`: a vendored vlt directory verified without its vendor ledger (from `vlt-lock.json` alone) holds a `package.json` with its devDependencies stripped, and the patched `package.json` blob is not in `.socket/blobs`, so it cannot be checked. A lockfile-pinned hosted reference with nothing installed attests instead of `package_not_found` (see "Manifest-less VEX"). |
12751275
| `lockfile_unreadable` / `lockfile_unparseable` / `patched_ref_invalid` / `patched_ref_unattributable` | run-level `warnings[]` | vex (every form): lockfile-discovery diagnostics — see "Manifest-less VEX (lockfile discovery)". Never flip the exit on their own. |
1276-
| `vendor_multiple_lockfiles` / `pypi_multiple_lockfiles` | `skipped` (warning) | vendor: a sibling lockfile of another package manager will still install UNPATCHED bytes; names the wired winner + the ignored locks. |
1276+
| `vendor_multiple_lockfiles` / `pypi_multiple_lockfiles` | `skipped` (warning) | vendor: a sibling lockfile of another package manager (for PyPI, also a root `requirements.txt` that pins the package beside the wired tool lock) will still install UNPATCHED bytes; names the wired winner + the ignored locks. |
12771277
| `vendor_yarn_berry_unsupported` | `failed` | vendor (npm): yarn-berry Plug'n'Play layout; use its native `yarn patch` workflow. |
12781278
| `vendor_bun_lockb_invalid` | `failed` | vendor / scan / get `--mode vendored`: the binary lock is malformed, unreadable, unsupported or cannot be rewritten safely. The detail names the parser, hash or filesystem error. Refused before patch downloads and before hosted takeover; `patches[]` / `download.patches[]` carry `errorCode` and `error`, while `get <uuid>` also carries top-level `error.code`. Dry-run predicts the same refusal. |
12791279
| `vendor_bun_workspace_unsupported` | `failed` | vendor / scan / get `--mode vendored` (bun): the text lock holds `workspace:` packages and its `lockfileVersion` is below 2 — Bun 1.2–1.3 resolve a workspace member's local-tarball path relative to the member; a committed version-2 lock is the proof every consumer runs Bun ≥ 1.4 (deliberate over-approximation: root-only declared packages would install on version 1 too). Detail names the version integer and a version-specific remedy: delete `bun.lock` and re-lock with Bun ≥ 1.4 (an in-place `bun install` keeps the existing version) — then, for a version-1 lock, "or use `--mode hosted`, which accepts version-1 workspace locks"; for a version-0 lock, "or delete `bun.lock`, re-lock with Bun ≥ 1.2 (which writes lockfileVersion 1) and use `--mode hosted`" (hosted refuses version-0 workspace locks, so a bare hosted pointer would send the user into a second refusal). Refused before any write — in the pre-download preflight on `get`/`scan` (see `vendor_bun_lockb_invalid` for the placements); in the shared preflight that `vendor` and the vendor step run BEFORE a hosted → vendored takeover's revert (a hosted-redirected purl stays hosted-wired, ledger and lock untouched; `vendor --dry-run` previews the same `failed` code); and in the engine when the run would write a NEW local tuple. Exempt: purls the vendor ledger wires at the selected uuid, purls whose every `bun.lock` instance is already a `.socket/vendor/npm/` tuple (any uuid), in-sync re-runs and `repair` redownloads. |

‎crates/socket-patch-cli/tests/mode_migration_pypi.rs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,3 +1489,54 @@ async fn platform_wheel_takeover_is_refused_before_revert() {
14891489
.await;
14901490
}
14911491
}
1492+
1493+
/// #612: a Pipenv project with a `requirements.txt` exported beside its
1494+
/// lock (`pipenv requirements`). Hosted mode pins both; the hosted →
1495+
/// vendored takeover (`vendor` over the hosted project) wires only the
1496+
/// governing `Pipfile.lock` and restores the requirements pin to upstream,
1497+
/// so the run must name `requirements.txt` among the install sources left
1498+
/// UNPATCHED instead of passing in silence.
1499+
#[tokio::test]
1500+
async fn pipenv_hosted_to_vendored_names_the_unpatched_requirements() {
1501+
let (_tmp, root) = project();
1502+
stage_pipenv(&root);
1503+
std::fs::write(
1504+
root.join("requirements.txt"),
1505+
"-i https://pypi.org/simple\nsix==1.16.0\n",
1506+
)
1507+
.unwrap();
1508+
let server = MockServer::start().await;
1509+
let hosted_url = mount_hosted_api(&server, true).await;
1510+
let (code, env) = hosted_scan(&root, &server);
1511+
assert_eq!(code, 0, "hosted scan: {env:#}");
1512+
let hosted_reqs = std::fs::read_to_string(root.join("requirements.txt")).unwrap();
1513+
assert!(
1514+
hosted_reqs.contains(&hosted_url),
1515+
"hosted mode pins requirements.txt too:\n{hosted_reqs}\n{env:#}"
1516+
);
1517+
1518+
stage_manifest(&root);
1519+
// The takeover recognizes the pin only under `--patch-server-url`,
1520+
// which also rehosts the prebuilt download: serve the vendored wheel
1521+
// from the same mock.
1522+
prebuilt_common::mount_project(&server, &root).await;
1523+
let uri = server.uri();
1524+
let (code, env) = run_cli(
1525+
&root,
1526+
&["vendor", "--patch-server-url", &uri, "--vendor-url", &uri],
1527+
&[],
1528+
);
1529+
assert_eq!(code, 0, "vendor takeover: {env:#}");
1530+
let lock = std::fs::read_to_string(root.join("Pipfile.lock")).unwrap();
1531+
assert!(
1532+
lock.contains(&format!(".socket/vendor/pypi/{UUID}/")),
1533+
"Pipfile.lock is wired to the vendored wheel:\n{lock}\n{env:#}"
1534+
);
1535+
let rendered = env.to_string();
1536+
assert!(
1537+
rendered.contains("\"pypi_multiple_lockfiles\"")
1538+
&& rendered.contains("wiring `Pipfile.lock`")
1539+
&& rendered.contains("requirements.txt will still install the UNPATCHED"),
1540+
"the takeover names requirements.txt as an unpatched install source: {env:#}"
1541+
);
1542+
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,16 @@ pub fn live_pnp_marker_with(
307307
/// (`crate::vendor::npm_flavor::detect_npm_lock_flavor`) so both
308308
/// detection sites agree on what counts as a pnpm-PnP tree.
309309
pub(crate) fn pnpm_pnp_layout(project_root: &Path) -> bool {
310-
let node_modules = project_root.join("node_modules");
311-
(node_modules.join(".modules.yaml").is_file() || node_modules.join(".pnpm").is_dir())
312-
&& project_root.join("pnpm-lock.yaml").is_file()
313-
&& !project_root.join("yarn.lock").is_file()
310+
pnpm_pnp_layout_in(&crate::vendor::lock_inventory::ProjectView::Disk(
311+
project_root,
312+
))
313+
}
314+
315+
/// [`pnpm_pnp_layout`] over a [`crate::vendor::lock_inventory::ProjectView`].
316+
pub(crate) fn pnpm_pnp_layout_in(view: &crate::vendor::lock_inventory::ProjectView<'_>) -> bool {
317+
(view.is_file("node_modules/.modules.yaml") || view.is_dir("node_modules/.pnpm"))
318+
&& view.is_file("pnpm-lock.yaml")
319+
&& !view.is_file("yarn.lock")
314320
}
315321

316322
/// The yarn Plug'n'Play loader of a project: the text of each loader file

‎crates/socket-patch-core/src/crawlers/python_crawler.rs‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,11 +603,13 @@ fn pdm_env_flag(var: &impl Fn(&str) -> Option<String>, name: &str) -> bool {
603603
}
604604

605605
/// Whether PDM installs the project at `cwd`: a PDM project (see
606-
/// [`is_pdm_project`], or a `.pdm-python`) with no `uv.lock` or
607-
/// `poetry.lock`, which drive installs ahead of `pdm.lock` (the hosted
608-
/// rewriters' precedence).
606+
/// [`is_pdm_project`], or a `.pdm-python`) with no tool lock that drives
607+
/// installs ahead of `pdm.lock` (`uv.lock`, `poetry.lock`: the shared
608+
/// precedence [`crate::formats::governing_locks::PYPI_TOOL_LOCKS`]).
609609
async fn pdm_drives_project(cwd: &Path) -> bool {
610-
if cwd.join("uv.lock").is_file() || cwd.join("poetry.lock").is_file() {
610+
if crate::formats::governing_locks::pypi_tool_lock_shadowed("pdm.lock", |lock| {
611+
cwd.join(lock).is_file()
612+
}) {
611613
return false;
612614
}
613615
cwd.join(".pdm-python").is_file() || is_pdm_project(cwd).await
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
//! Which lockfile governs a project's installs, per ecosystem: the ONE
2+
//! precedence table every mode reads.
3+
//!
4+
//! Before this table the npm-family order lived in the vendored router, its
5+
//! in-memory copy, the inventory's migration-leftover fallback, the hosted
6+
//! vlt `SIBLING_LOCKS` list, the hosted vlt preflight inputs, the hosted
7+
//! npm rewriter's "another lock owns it" check and the hosted governing-root
8+
//! lock-root and workspace-root lists; the PyPI order lived in the
9+
//! vendored router, the hosted `pdm_drives` gate and the agent-mode PDM
10+
//! crawler. Each was a hand copy, and a precedence change had to land in all
11+
//! of them.
12+
//!
13+
//! The table is presence-only and PURE: callers stat the files (on disk, in
14+
//! a snapshot or in a memory project) and pass a predicate. Content
15+
//! decisions that refine a family (pnpm v9 vs legacy, yarn classic vs berry,
16+
//! a vlt lock's version) stay with the router that reads the bytes.
17+
//!
18+
//! What each mode DOES with the answer is deliberately different, and stays
19+
//! with the mode: vendored wires the governing lock only and warns about
20+
//! every other present lock ([`npm_locks_outside`],
21+
//! [`pypi_locks_outside`]); hosted rewrites every present lock and asks the
22+
//! table only to decide which lock confirms a pin or may veto its siblings
23+
//! ([`pypi_tool_lock_governs`], and vlt's `vlt_drives`).
24+
25+
use crate::constants::npm_family::{BUN_LOCK, BUN_LOCKB, NPM_LOCKS, PNPM_LOCK, VLT_LOCK};
26+
27+
/// One npm-family wiring family: the locks one package manager installs
28+
/// from. The family that governs wires (or supersedes) every file in it.
29+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30+
pub enum NpmLockFamily {
31+
/// `vlt-lock.json`.
32+
Vlt,
33+
/// `bun.lock` and the binary `bun.lockb` (Bun reads the text lock when
34+
/// both exist).
35+
Bun,
36+
/// The root `pnpm-lock.yaml`.
37+
Pnpm,
38+
/// `yarn.lock`, classic or berry.
39+
Yarn,
40+
/// `npm-shrinkwrap.json` and `package-lock.json` (npm prefers the
41+
/// shrinkwrap; npm 12 installs from the package-lock beside it).
42+
Npm,
43+
}
44+
45+
impl NpmLockFamily {
46+
/// The root-relative lock files of this family, in its own preference
47+
/// order.
48+
pub const fn files(self) -> &'static [&'static str] {
49+
match self {
50+
NpmLockFamily::Vlt => &[VLT_LOCK],
51+
NpmLockFamily::Bun => &[BUN_LOCK, BUN_LOCKB],
52+
NpmLockFamily::Pnpm => &[PNPM_LOCK],
53+
NpmLockFamily::Yarn => &["yarn.lock"],
54+
NpmLockFamily::Npm => &NPM_LOCKS,
55+
}
56+
}
57+
}
58+
59+
/// The npm-family precedence, first present family wins. A Plug'n'Play
60+
/// loader is checked before any of these (it refuses or reclassifies the
61+
/// project whatever locks are present), and Rush's common lock only after
62+
/// none matched; both stay with the router.
63+
///
64+
/// vlt first: a committed `vlt-lock.json` is only ever written by vlt. Bun
65+
/// before pnpm: Bun's isolated linker leaves a `node_modules/.bun/` store
66+
/// that looks like pnpm's, so the lock name decides.
67+
pub const NPM_PRECEDENCE: [NpmLockFamily; 5] = [
68+
NpmLockFamily::Vlt,
69+
NpmLockFamily::Bun,
70+
NpmLockFamily::Pnpm,
71+
NpmLockFamily::Yarn,
72+
NpmLockFamily::Npm,
73+
];
74+
75+
/// Every root npm-family lock file, in precedence order.
76+
pub fn npm_lock_files() -> impl Iterator<Item = &'static str> {
77+
NPM_PRECEDENCE
78+
.into_iter()
79+
.flat_map(|family| family.files().iter().copied())
80+
}
81+
82+
/// The family whose lock governs installs: the first in
83+
/// [`NPM_PRECEDENCE`] with any file present, skipping `skip` (the inventory
84+
/// asks what a version-refused pnpm lock was shadowing).
85+
pub fn npm_governing_family(
86+
present: impl Fn(&str) -> bool,
87+
skip: Option<NpmLockFamily>,
88+
) -> Option<NpmLockFamily> {
89+
NPM_PRECEDENCE
90+
.into_iter()
91+
.filter(|family| Some(*family) != skip)
92+
.find(|family| family.files().iter().any(|file| present(file)))
93+
}
94+
95+
/// The present lock files OUTSIDE `family`, in precedence order: the locks
96+
/// a single-lock wiring leaves untouched.
97+
pub fn npm_locks_outside(
98+
family: NpmLockFamily,
99+
present: impl Fn(&str) -> bool,
100+
) -> Vec<&'static str> {
101+
NPM_PRECEDENCE
102+
.into_iter()
103+
.filter(|other| *other != family)
104+
.flat_map(|other| other.files().iter().copied())
105+
.filter(|file| present(file))
106+
.collect()
107+
}
108+
109+
/// The PyPI tool lockfiles, in precedence order (migration direction and
110+
/// ecosystem currency: uv > Poetry > PDM > Pipenv). Standalone PEP 751 /
111+
/// PEP 723 locks rank between uv and Poetry, but only when they pin the
112+
/// package being wired, so the vendored router decides them with the
113+
/// content in hand. `requirements.txt` and Hatch rank below every tool
114+
/// lock.
115+
pub const PYPI_TOOL_LOCKS: [&str; 4] = ["uv.lock", "poetry.lock", "pdm.lock", "Pipfile.lock"];
116+
117+
/// The PyPI requirements file the vendored router falls back to.
118+
pub const PYPI_REQUIREMENTS: &str = "requirements.txt";
119+
120+
/// The governing tool lock: the first of [`PYPI_TOOL_LOCKS`] present.
121+
pub fn pypi_governing_tool_lock(present: impl Fn(&str) -> bool) -> Option<&'static str> {
122+
PYPI_TOOL_LOCKS.into_iter().find(|lock| present(lock))
123+
}
124+
125+
/// Whether a tool lock of higher precedence than `lock` (one of
126+
/// [`PYPI_TOOL_LOCKS`]) is present: `lock`'s tool does not drive installs,
127+
/// whether or not `lock` itself exists yet.
128+
pub fn pypi_tool_lock_shadowed(lock: &str, present: impl Fn(&str) -> bool) -> bool {
129+
PYPI_TOOL_LOCKS
130+
.into_iter()
131+
.take_while(|higher| *higher != lock)
132+
.any(present)
133+
}
134+
135+
/// Whether `lock` (one of [`PYPI_TOOL_LOCKS`]) is present and no tool lock
136+
/// of higher precedence is: a leftover lower-ranked lock neither drives
137+
/// installs nor may veto the governing one.
138+
pub fn pypi_tool_lock_governs(lock: &str, present: impl Fn(&str) -> bool) -> bool {
139+
present(lock) && !pypi_tool_lock_shadowed(lock, present)
140+
}
141+
142+
/// The present tool locks other than the governing one, in precedence
143+
/// order.
144+
pub fn pypi_locks_outside(governing: &str, present: impl Fn(&str) -> bool) -> Vec<&'static str> {
145+
PYPI_TOOL_LOCKS
146+
.into_iter()
147+
.filter(|lock| *lock != governing && present(lock))
148+
.collect()
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use super::*;
154+
155+
fn set<'a>(files: &'a [&'a str]) -> impl Fn(&str) -> bool + 'a {
156+
move |name| files.contains(&name)
157+
}
158+
159+
#[test]
160+
fn npm_precedence_is_vlt_bun_pnpm_yarn_npm() {
161+
let all = [
162+
"package-lock.json",
163+
"npm-shrinkwrap.json",
164+
"yarn.lock",
165+
"pnpm-lock.yaml",
166+
"bun.lockb",
167+
"bun.lock",
168+
"vlt-lock.json",
169+
];
170+
let expected = [
171+
(NpmLockFamily::Vlt, 6),
172+
(NpmLockFamily::Bun, 4),
173+
(NpmLockFamily::Pnpm, 3),
174+
(NpmLockFamily::Yarn, 2),
175+
(NpmLockFamily::Npm, 0),
176+
];
177+
for (family, from) in expected {
178+
assert_eq!(
179+
npm_governing_family(set(&all[..=from]), None),
180+
Some(family),
181+
"{:?}",
182+
&all[..=from]
183+
);
184+
}
185+
assert_eq!(npm_governing_family(set(&[]), None), None);
186+
assert_eq!(
187+
npm_governing_family(set(&["bun.lockb"]), None),
188+
Some(NpmLockFamily::Bun)
189+
);
190+
assert_eq!(
191+
npm_governing_family(set(&["package-lock.json"]), None),
192+
Some(NpmLockFamily::Npm)
193+
);
194+
}
195+
196+
#[test]
197+
fn skip_reports_what_a_family_shadows() {
198+
let files = ["pnpm-lock.yaml", "yarn.lock", "package-lock.json"];
199+
assert_eq!(
200+
npm_governing_family(set(&files), Some(NpmLockFamily::Pnpm)),
201+
Some(NpmLockFamily::Yarn)
202+
);
203+
assert_eq!(
204+
npm_governing_family(set(&["pnpm-lock.yaml"]), Some(NpmLockFamily::Pnpm)),
205+
None
206+
);
207+
}
208+
209+
#[test]
210+
fn locks_outside_a_family_never_include_its_own_files() {
211+
let files = [
212+
"vlt-lock.json",
213+
"bun.lock",
214+
"bun.lockb",
215+
"pnpm-lock.yaml",
216+
"yarn.lock",
217+
"npm-shrinkwrap.json",
218+
"package-lock.json",
219+
];
220+
assert_eq!(npm_lock_files().collect::<Vec<_>>(), files);
221+
assert_eq!(
222+
npm_locks_outside(NpmLockFamily::Vlt, set(&files)),
223+
files[1..]
224+
);
225+
assert_eq!(
226+
npm_locks_outside(NpmLockFamily::Npm, set(&files)),
227+
files[..5]
228+
);
229+
assert!(npm_locks_outside(NpmLockFamily::Bun, set(&["bun.lock", "bun.lockb"])).is_empty());
230+
}
231+
232+
#[test]
233+
fn pypi_tool_lock_precedence() {
234+
assert_eq!(
235+
pypi_governing_tool_lock(set(&["Pipfile.lock", "pdm.lock", "uv.lock"])),
236+
Some("uv.lock")
237+
);
238+
assert!(pypi_tool_lock_governs(
239+
"pdm.lock",
240+
set(&["pdm.lock", "Pipfile.lock"])
241+
));
242+
assert!(!pypi_tool_lock_governs(
243+
"pdm.lock",
244+
set(&["pdm.lock", "poetry.lock"])
245+
));
246+
assert!(!pypi_tool_lock_governs(
247+
"pdm.lock",
248+
set(&["pdm.lock", "uv.lock"])
249+
));
250+
assert!(!pypi_tool_lock_governs("pdm.lock", set(&[])));
251+
assert!(pypi_tool_lock_shadowed("pdm.lock", set(&["poetry.lock"])));
252+
assert!(!pypi_tool_lock_shadowed("pdm.lock", set(&["Pipfile.lock"])));
253+
assert!(!pypi_tool_lock_shadowed(
254+
"uv.lock",
255+
set(&["uv.lock", "poetry.lock"])
256+
));
257+
assert_eq!(
258+
pypi_locks_outside(
259+
"poetry.lock",
260+
set(&["uv.lock", "poetry.lock", "Pipfile.lock"])
261+
),
262+
["uv.lock", "Pipfile.lock"]
263+
);
264+
}
265+
}

‎crates/socket-patch-core/src/formats/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
pub(crate) mod bun;
3030
pub mod cargo;
3131
pub mod composer;
32+
pub mod governing_locks;
3233
pub mod gem;
3334
pub(crate) mod maven;
3435
pub(crate) mod nuget;

0 commit comments

Comments
 (0)