1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! End-to-end: `socket-patch apply` against a yarn-berry PnP layout
//! must refuse with a clear `errorCode: yarn_pnp_unsupported`.
//!
//! yarn-berry's Plug'n'Play mode keeps packages inside
//! `.yarn/cache/*.zip` and resolves them via a custom Node loader
//! (`.pnp.cjs`). socket-patch cannot rewrite bytes inside a zip in
//! place; the right move is to refuse with a clear pointer to
//! `yarn patch`.
//!
//! The matching unit tests
//! (`crates/socket-patch-core/src/crawlers/pkg_managers.rs`) pin the
//! detection table. This test composes the detection with the apply
//! CLI to verify the end-to-end refusal.
//!
//! Network: no. Toolchain: no. NOT `#[ignore]` — runs on every PR.
use std::path::Path;
#[path = "common/mod.rs"]
mod common;
use common::{
assert_run_ok, envelope_error_code, envelope_error_message, json_string,
parse_json_envelope, run, write_minimal_manifest, PatchEntry,
};
/// Stage the minimum filesystem layout the detector classifies as
/// yarn-berry PnP: a `.pnp.cjs` file at the project root plus a
/// `.yarn/cache/` directory. The presence of `.pnp.cjs` alone is
/// enough for the detector, but ship the cache dir too so the
/// fixture mirrors what an actual yarn-berry checkout looks like.
fn make_yarn_berry_project(cwd: &Path) {
std::fs::write(
cwd.join("package.json"),
r#"{"name":"yarn-berry-fixture","version":"0.0.0","private":true}"#,
)
.expect("write package.json");
std::fs::write(cwd.join(".pnp.cjs"), b"// stub PnP loader\n")
.expect("write .pnp.cjs");
std::fs::create_dir_all(cwd.join(".yarn").join("cache"))
.expect("create .yarn/cache");
}
/// Manifest with a single trivial patch entry. The actual hashes
/// don't matter — apply refuses on layout detection before any
/// hash check.
fn write_synthetic_manifest(socket_dir: &Path) {
write_minimal_manifest(
socket_dir,
"pkg:npm/dummy@1.0.0",
"11111111-1111-4111-8111-111111111111",
&[PatchEntry {
file_name: "package/index.js",
before_hash: "a".repeat(64).as_str(),
after_hash: "b".repeat(64).as_str(),
}],
);
}
/// The headline test: yarn-berry PnP project + apply = exit 1 with
/// `errorCode: yarn_pnp_unsupported`. JSON envelope so consumers can
/// branch deterministically on the error code.
#[test]
fn yarn_pnp_refuses_with_error_code() {
let dir = tempfile::tempdir().unwrap();
make_yarn_berry_project(dir.path());
write_synthetic_manifest(&dir.path().join(".socket"));
let (code, stdout, stderr) = run(dir.path(), &["apply", "--json"]);
assert_eq!(
code, 1,
"expected exit 1.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let env = parse_json_envelope(&stdout);
assert_eq!(
envelope_error_code(&env),
Some("yarn_pnp_unsupported"),
"expected error.code=yarn_pnp_unsupported.\nenvelope: {env}"
);
assert_eq!(
json_string(&env, "status"),
Some("error"),
"expected status=error.\nenvelope: {env}"
);
// The error message must mention `yarn patch` so the user knows
// the workaround. Contract: this is part of the public CLI
// output — don't loosen the assertion without intent.
let error_msg = envelope_error_message(&env).unwrap_or("");
assert!(
error_msg.contains("yarn patch"),
"error message should point at `yarn patch`, got: {error_msg}"
);
}
/// Human-output mode: same project, no `--json`. Apply still exits
/// 1; the stderr stream must mention `yarn patch` so a human reader
/// gets the same workaround pointer.
#[test]
fn yarn_pnp_refuses_in_human_mode() {
let dir = tempfile::tempdir().unwrap();
make_yarn_berry_project(dir.path());
write_synthetic_manifest(&dir.path().join(".socket"));
let (code, _stdout, stderr) = run(dir.path(), &["apply"]);
assert_eq!(code, 1);
assert!(
stderr.contains("yarn patch"),
"stderr should point at `yarn patch`, got:\n{stderr}"
);
}
/// Negative control: a plain npm layout (no `.pnp.cjs`) must NOT
/// surface the yarn-pnp error code. The apply may still fail for
/// unrelated reasons (no matching packages on disk, etc.) — we
/// specifically assert the error code is NOT
/// `yarn_pnp_unsupported`.
#[test]
fn npm_layout_does_not_trigger_yarn_pnp_refusal() {
let dir = tempfile::tempdir().unwrap();
// Plain npm: package.json + an empty node_modules/ — no
// .pnp.cjs, no .yarn/cache/.
std::fs::write(
dir.path().join("package.json"),
r#"{"name":"npm-fixture","version":"0.0.0","private":true}"#,
)
.unwrap();
std::fs::create_dir_all(dir.path().join("node_modules")).unwrap();
write_synthetic_manifest(&dir.path().join(".socket"));
let (_code, stdout, _stderr) = run(dir.path(), &["apply", "--json"]);
// The output may or may not parse as a single JSON object
// depending on what apply printed (the synthetic manifest
// points at packages that don't exist on disk; apply may
// succeed-with-skipped or fail). All we assert here: the
// yarn-pnp error code MUST NOT appear in the output.
assert!(
!stdout.contains("yarn_pnp_unsupported"),
"npm layout should not trigger yarn-pnp refusal.\nstdout:\n{stdout}"
);
}
/// `.pnp.loader.mjs` (the ESM variant) also triggers the same
/// refusal. Pinning this in case the detection table drifts and
/// only the `.cjs` form keeps working.
#[test]
fn yarn_pnp_loader_mjs_also_refuses() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"name":"yarn-berry-esm","version":"0.0.0","private":true}"#,
)
.unwrap();
// ESM PnP loader variant — newer yarn-berry installs ship this
// instead of `.pnp.cjs`.
std::fs::write(
dir.path().join(".pnp.loader.mjs"),
b"// stub PnP ESM loader\n",
)
.unwrap();
write_synthetic_manifest(&dir.path().join(".socket"));
let (code, stdout, _stderr) = run(dir.path(), &["apply", "--json"]);
assert_eq!(code, 1);
let env = parse_json_envelope(&stdout);
assert_eq!(
envelope_error_code(&env),
Some("yarn_pnp_unsupported")
);
}
/// A guard test asserting the helper itself produced a manifest
/// the CLI can find. Without this, a refactor that breaks
/// `write_minimal_manifest` would make every other test in this
/// file pass by accident (apply would exit on "no manifest" rather
/// than on yarn-pnp detection). Running `apply` against a plain
/// project where the manifest exists but yarn-pnp markers are
/// absent should NOT report "no manifest".
#[test]
fn synthetic_manifest_is_discovered_by_cli() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"name":"plain","version":"0.0.0","private":true}"#,
)
.unwrap();
write_synthetic_manifest(&dir.path().join(".socket"));
// `list` doesn't apply, doesn't acquire the lock, doesn't
// detect package managers — it just reads the manifest. If
// our synthetic manifest is well-formed, list prints it.
let (stdout, _stderr) = assert_run_ok(dir.path(), &["list", "--json"], "list --json");
assert!(
stdout.contains("pkg:npm/dummy@1.0.0"),
"list should surface our synthetic manifest entry, got:\n{stdout}"
);
}