Skip to content

Commit 73184f5

Browse files
committed
Document uv export co-wiring in the contract
The pypi_multiple_lockfiles row now says an exact pin in a uv export requirements.txt is wired with uv.lock, as one beside Pipfile.lock is, and the #1120 CLI test keeps covering the UTF-16 exports that the requirements wiring still refuses. Refs #1368 Assisted-by: Claude Code:claude-opus-5-5
1 parent c58f599 commit 73184f5

2 files changed

Lines changed: 65 additions & 13 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
14181418
| `redirect_unconfirmed` | `skipped` (`details.mode: "hosted"`) | hosted `scan` / `get` (v5.0, additive): the patch was granted but no lockfile entry pinning it could be rewritten. The status and exit code are unchanged for now, pending the open hosted exit-policy decision (#704); `--silent` hides the human line. |
14191419
| `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. |
14201420
| `vex_npm_shrinkwrap_only` | run warning and `failed[].reason` | vex (every form, #899): the patch is wired only in a root `npm-shrinkwrap.json` with no `package-lock.json` twin, which npm >= 12 never reads; no statement until the twin exists. `list` / `rollback` / `remove` still manage the wiring. |
1421-
| `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. Beside `Pipfile.lock`, an exact registry pin of the package in `requirements.txt` or an in-root `-r` include (`pipenv requirements` output) is wired with the lock instead and never named here (#612): both files then refer to the committed wheel, the ledger entry records both, every revert restores both, and a re-run over a project whose `Pipfile.lock` is already wired (a vendor from before, or an export made since) wires the export too (`pypi_requirements_sibling_wired`). A pin the requirements wiring cannot rewrite (a range, extras, an include outside the root) stays a loser. |
1421+
| `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. Beside `Pipfile.lock` or `uv.lock`, an exact registry pin of the package in `requirements.txt` or an in-root `-r` include (`pipenv requirements` or `uv export` output) is wired with the lock instead and never named here (#612, #1368): both files then refer to the committed wheel, the ledger entry records both, every revert restores both, a superseding patch re-wires both, and a re-run over a project whose lock is already wired (a vendor from before, or an export made since) wires the export too (`pypi_requirements_sibling_wired`). A pin the requirements wiring cannot rewrite (a range, extras, an include outside the root) stays a loser. |
14221422
| `vendor_npm_shrinkwrap_only` | `skipped` (warning) | vendor / scan / get `--mode vendored` (npm, #899): the package was wired into `npm-shrinkwrap.json`, the only npm lock (also on an in-sync re-run). npm >= 12 never reads the shrinkwrap and installs the unpatched registry bytes; rename the lock to `package-lock.json` (or commit a copy under that name) and re-run. |
14231423
| `vendor_workspace_member_skipped` | `skipped` (warning) | vendor / scan / get `--mode vendored` (npm, #688): a lock entry with the package's `name@version` is the project's own source (a workspace member or a `file:` directory), so it is left alone while the lock's registry copies are vendored; patch that source directly if it needs the fix. When it is the only instance, vendoring refuses instead. |
14241424
| `vendor_npm_allow_file` | `skipped` (warning, printed as `Warning (vendor_npm_allow_file)`) | vendor / scan / get `--mode vendored` (package-lock, #969): the effective npm `allow-file` setting (env > project `.npmrc` > user > global > builtin) is not `all`, so npm >= 11.14 refuses the vendored `file:` tarball (EALLOWFILE) on install. The setting is respected, never rewritten; the detail names where it comes from and the remedy (`allow-file=all`). `vendor --check` fails the entry for the same reason. |

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

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,15 +1625,71 @@ async fn vendor_check_fails_after_hatch_dependency_reset() {
16251625
assert_check_catches_relock(&root, files);
16261626
}
16271627

1628+
/// #1368: a `uv export` requirements.txt beside `uv.lock` is an install
1629+
/// source of its own (Docker / plain pip). `vendor` wires its exact pin to
1630+
/// the same committed wheel as uv.lock, `vendor --check` passes with no
1631+
/// contest, a re-run is settled, and `vendor --revert` restores all three
1632+
/// files byte for byte.
1633+
#[tokio::test]
1634+
async fn uv_requirements_export_is_wired_with_the_lock() {
1635+
let export = "six==1.16.0 \\\n --hash=sha256:WHEEL_SHA\n".replace("WHEEL_SHA", WHEEL_SHA);
1636+
let (_tmp, root) = project();
1637+
let files = stage_uv(&root);
1638+
std::fs::write(root.join("requirements.txt"), &export).unwrap();
1639+
let originals: Vec<(&str, Vec<u8>)> = files
1640+
.iter()
1641+
.chain(&["requirements.txt"])
1642+
.map(|f| (*f, std::fs::read(root.join(f)).unwrap()))
1643+
.collect();
1644+
stage_manifest(&root);
1645+
let needle = format!(".socket/vendor/pypi/{UUID}/");
1646+
1647+
let (code, env) = run_cli(&root, &["vendor"], &[]);
1648+
assert_eq!(code, 0, "{env:#}");
1649+
for file in ["uv.lock", "requirements.txt"] {
1650+
assert!(
1651+
std::fs::read_to_string(root.join(file))
1652+
.unwrap()
1653+
.contains(&needle),
1654+
"{file} is wired: {env:#}"
1655+
);
1656+
}
1657+
assert!(
1658+
!env.to_string().contains("pypi_multiple_lockfiles"),
1659+
"the export is wired, not an unpatched loser: {env:#}"
1660+
);
1661+
1662+
let (code, env) = run_cli(&root, &["vendor", "--check"], &[]);
1663+
assert_eq!(code, 0, "both install sources are wired: {env:#}");
1664+
1665+
// Re-running is settled: nothing rewritten.
1666+
let wired = std::fs::read(root.join("requirements.txt")).unwrap();
1667+
let (code, env) = run_cli(&root, &["vendor"], &[]);
1668+
assert_eq!(code, 0, "{env:#}");
1669+
assert_eq!(std::fs::read(root.join("requirements.txt")).unwrap(), wired);
1670+
1671+
let (code, env) = run_cli(&root, &["vendor", "--revert"], &[]);
1672+
assert_eq!(code, 0, "{env:#}");
1673+
for (file, bytes) in &originals {
1674+
assert_eq!(
1675+
&std::fs::read(root.join(file)).unwrap(),
1676+
bytes,
1677+
"{file} is restored"
1678+
);
1679+
}
1680+
assert!(!root.join(format!(".socket/vendor/pypi/{UUID}")).exists());
1681+
}
1682+
16281683
/// #1120: a `requirements.txt` exported beside `uv.lock` pins the
1629-
/// package from PyPI, so the uv-only wiring leaves installs from it
1630-
/// unpatched. `uv export > requirements.txt` in Windows PowerShell 5.1
1631-
/// writes it as UTF-16 with a byte-order mark, which pip and uv read like
1632-
/// its UTF-8 twin, so `vendor` must name it (`pypi_multiple_lockfiles`)
1633-
/// and `vendor --check` must fail on the contested wiring in every
1634-
/// encoding, not pass as if the file were absent.
1684+
/// package from PyPI. `uv export > requirements.txt` in Windows
1685+
/// PowerShell 5.1 writes it as UTF-16 with a byte-order mark, which pip
1686+
/// and uv read like its UTF-8 twin but the requirements wiring refuses to
1687+
/// rewrite, so the uv-only wiring leaves installs from it unpatched:
1688+
/// `vendor` must name it (`pypi_multiple_lockfiles`) and `vendor --check`
1689+
/// must fail on the contested wiring, not pass as if the file were absent.
1690+
/// (Its UTF-8 twin is wired with the lock, #1368.)
16351691
#[tokio::test]
1636-
async fn uv_requirements_export_contests_the_wiring_in_any_encoding() {
1692+
async fn uv_requirements_export_contests_the_wiring_in_utf16() {
16371693
let text = "six==1.16.0\r\n";
16381694
let le: Vec<u8> = [0xFF, 0xFE]
16391695
.into_iter()
@@ -1643,11 +1699,7 @@ async fn uv_requirements_export_contests_the_wiring_in_any_encoding() {
16431699
.into_iter()
16441700
.chain(text.encode_utf16().flat_map(u16::to_be_bytes))
16451701
.collect();
1646-
for (case, bytes) in [
1647-
("utf-8", text.as_bytes().to_vec()),
1648-
("utf-16 le", le),
1649-
("utf-16 be", be),
1650-
] {
1702+
for (case, bytes) in [("utf-16 le", le), ("utf-16 be", be)] {
16511703
let (_tmp, root) = project();
16521704
stage_uv(&root);
16531705
std::fs::write(root.join("requirements.txt"), &bytes).unwrap();

0 commit comments

Comments
 (0)