Skip to content

Commit b06c36c

Browse files
committed
ci: fix historical Yarn and vlt upstream restore expectations
1 parent a5a9fb9 commit b06c36c

4 files changed

Lines changed: 99 additions & 35 deletions

File tree

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

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ fn hosted_leg(name: &'static str) -> Option<Leg> {
2929
Leg::start(SUITE, name)
3030
}
3131

32+
/// v5 restores upstream pins without a saved lock fragment. The earliest
33+
/// vlt releases record npmjs URLs even with the harness registry configured;
34+
/// restore may omit that redundant slot or point it at the harness registry.
35+
/// All other bytes, including bystanders and line endings, must still match.
36+
fn assert_restored_lock(fx: &Fixture, before: &[u8]) {
37+
let mut expected = String::from_utf8(before.to_vec()).unwrap();
38+
let mut actual = String::from_utf8(lock_bytes(&fx.proj)).unwrap();
39+
if fx.leg.version() <= VltVersion::zero(11) {
40+
for target in &fx.svc.targets {
41+
let bare = target.name.rsplit('/').next().unwrap();
42+
let path = Registry::tarball_path(&target.name, bare, &target.version);
43+
expected = expected.replace(&format!(",\"https://registry.npmjs.org{path}\""), "");
44+
actual = actual.replace(&format!(",\"{}{path}\"", fx.reg.server.uri()), "");
45+
}
46+
}
47+
assert_eq!(actual, expected, "rollback restores the upstream lock");
48+
}
49+
3250
// ── drivers and fresh checkouts ───────────────────────────────────────────
3351

3452
/// `scan --mode hosted --vex`: the lock pins the artifact (one preflight
@@ -167,7 +185,7 @@ async fn vlt_pinned_matrix_hosted_tamper_cold_eintegrity() {
167185

168186
// ── rollback, rerun, heal ─────────────────────────────────────────────────
169187

170-
/// Rollback restores the lock byte-for-byte, heals the patched store copy
188+
/// Rollback restores the upstream lock, heals the patched store copy
171189
/// (and nothing else), and the next `vlt install` is pristine.
172190
#[tokio::test(flavor = "multi_thread")]
173191
#[ignore = "real vlt: SOCKET_PATCH_VLT_E2E_JS"]
@@ -184,11 +202,7 @@ async fn vlt_pinned_matrix_hosted_rollback_byte_exact() {
184202
let out = fx.rollback(&[]);
185203
assert_eq!(out.code, 0, "{out}");
186204
let doc = out.json();
187-
assert_eq!(
188-
String::from_utf8_lossy(&lock_bytes(&fx.proj)),
189-
String::from_utf8_lossy(&fx.lock_before),
190-
"rollback restores vlt-lock.json byte-for-byte"
191-
);
205+
assert_restored_lock(&fx, &fx.lock_before);
192206
assert!(
193207
fx.ledger().is_none(),
194208
"no hosted ledger is ever written (v5)"
@@ -762,11 +776,10 @@ async fn vlt_pinned_matrix_hosted_crlf_lock() {
762776
assert_eq!(state(&co, fx.t()), State::Patched);
763777
let out = fx.rollback(&[]);
764778
assert_eq!(out.code, 0, "{out}");
765-
assert_eq!(
766-
lock_bytes(&fx.proj),
767-
crlf,
768-
"rollback restores the CRLF lock"
769-
);
779+
assert_restored_lock(&fx, &crlf);
780+
let co = fx.checkout("restored-crlf");
781+
fx.vlt_ok_profile(&co, &fx.leg.locked_install_args(), "restored-crlf");
782+
assert_eq!(state(&co, fx.t()), State::Pristine);
770783
fx.leg.ran();
771784
}
772785

@@ -1185,11 +1198,7 @@ async fn vlt_pinned_matrix_hosted_idempotence() {
11851198
assert!(fx.ledger().is_none());
11861199
let out = fx.rollback(&[]);
11871200
assert_eq!(out.code, 0, "{out}");
1188-
assert_eq!(
1189-
String::from_utf8_lossy(&lock_bytes(&fx.proj)),
1190-
String::from_utf8_lossy(&fx.lock_before),
1191-
"rollback restores the registry lock byte-for-byte: {out}"
1192-
);
1201+
assert_restored_lock(&fx, &fx.lock_before);
11931202
assert!(fx.ledger().is_none());
11941203
let files = package_files(&fx.proj);
11951204
let out = fx.rollback(&[]);
@@ -1199,6 +1208,9 @@ async fn vlt_pinned_matrix_hosted_idempotence() {
11991208
"a second rollback finds no state: {out}"
12001209
);
12011210
assert_eq!(package_files(&fx.proj), files, "and writes nothing");
1211+
let co = fx.checkout("restored-idempotence");
1212+
fx.vlt_ok_profile(&co, &fx.leg.locked_install_args(), "restored-idempotence");
1213+
assert_eq!(state(&co, fx.t()), State::Pristine);
12021214
fx.leg.ran();
12031215
}
12041216

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

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,15 @@ async fn mount_hosted_mocks(
388388
/// Serve, from `server` (as `SOCKET_NPM_REGISTRY`), the npm registry version
389389
/// document the v5 upstream restore reads for DEP — mirrored from what the
390390
/// PRISTINE classic lock recorded (`resolved "<tarball>#<sha1>"`,
391-
/// `integrity`). The restore of a hosted classic entry must reproduce the
392-
/// registry entry yarn wrote from exactly that document; mirroring it keeps
391+
/// `integrity`, or the SHA-1 fragment on pre-1.10 releases). The restore must
392+
/// reproduce the registry entry yarn wrote from that document; mirroring it keeps
393393
/// the unwind hermetic (the binary's TLS stack need not reach the real
394-
/// registry). Returns the registry base to hand the binary.
395-
async fn mount_registry_from_classic_lock(server: &MockServer, lock: &str) -> String {
394+
/// registry). Returns the registry base and expected upstream lock. Hosted
395+
/// mode adds an integrity line even on pre-1.10 yarn, and v5 restores that
396+
/// line's registry hash without a saved fragment to recover its absence.
397+
async fn mount_registry_from_classic_lock(server: &MockServer, lock: &str) -> (String, String) {
398+
use base64::Engine as _;
399+
396400
let block = lock
397401
.split("\n\n")
398402
.find(|b| {
@@ -404,23 +408,43 @@ async fn mount_registry_from_classic_lock(server: &MockServer, lock: &str) -> St
404408
.lines()
405409
.find_map(|l| l.trim().strip_prefix(&format!("{name} ")))
406410
.map(|v| v.trim_matches('"').to_string())
407-
.unwrap_or_else(|| panic!("no `{name}` in {block}"))
408411
};
409-
let resolved = field("resolved");
412+
let resolved = field("resolved").unwrap_or_else(|| panic!("no `resolved` in {block}"));
410413
let (tarball, shasum) = resolved
411414
.split_once('#')
412415
.map(|(t, s)| (t.to_string(), Some(s.to_string())))
413416
.unwrap_or((resolved.clone(), None));
417+
let integrity = field("integrity").unwrap_or_else(|| {
418+
let sha1 = hex::decode(
419+
shasum
420+
.as_ref()
421+
.expect("pre-1.10 yarn pins a SHA-1 fragment"),
422+
)
423+
.expect("the resolved fragment is hex SHA-1");
424+
format!(
425+
"sha1-{}",
426+
base64::engine::general_purpose::STANDARD.encode(sha1)
427+
)
428+
});
429+
let upstream_lock = if field("integrity").is_some() {
430+
lock.to_string()
431+
} else {
432+
lock.replacen(
433+
&format!(" resolved \"{resolved}\""),
434+
&format!(" resolved \"{resolved}\"\n integrity {integrity}"),
435+
1,
436+
)
437+
};
414438
Mock::given(method("GET"))
415439
.and(path(format!("/registry/{DEP}/{DEP_VERSION}")))
416440
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
417441
"name": DEP,
418442
"version": DEP_VERSION,
419-
"dist": { "tarball": tarball, "integrity": field("integrity"), "shasum": shasum }
443+
"dist": { "tarball": tarball, "integrity": integrity, "shasum": shasum }
420444
})))
421445
.mount(server)
422446
.await;
423-
format!("{}/registry", server.uri())
447+
(format!("{}/registry", server.uri()), upstream_lock)
424448
}
425449

426450
fn run_hosted_scan(proj: &Path, server_uri: &str) -> (i32, String, String) {
@@ -642,7 +666,7 @@ fn assert_pure_vendored_and_round_trip(
642666
"fresh vendored install must carry the PATCHED bytes ({tag})"
643667
);
644668

645-
// (b) Round trip: `vendor --revert` restores the REGISTRY lock
669+
// (b) Round trip: `vendor --revert` restores the expected REGISTRY lock
646670
// byte-identically (pre-fix it restored the hosted fragment, with no CLI
647671
// path back to registry state).
648672
let (code, stdout, stderr) = run_socket(
@@ -659,8 +683,8 @@ fn assert_pure_vendored_and_round_trip(
659683
assert_eq!(
660684
std::fs::read(proj.join("yarn.lock")).unwrap(),
661685
lock_pristine,
662-
"yarn.lock must be restored byte-identical to the pre-hosted \
663-
REGISTRY pristine ({tag}); got:\n{}",
686+
"yarn.lock must be restored byte-identical to the expected \
687+
upstream REGISTRY lock ({tag}); got:\n{}",
664688
read(proj, "yarn.lock")
665689
);
666690
assert_eq!(
@@ -809,7 +833,7 @@ async fn classic_hosted_then_vendored_takeover_round_trips_to_registry() {
809833
// upstream restore re-resolves the registry entry (mirrored from the
810834
// pristine lock), and the mock origin is named hosted via
811835
// --patch-server-url.
812-
let registry =
836+
let (registry, lock_upstream) =
813837
mount_registry_from_classic_lock(&server, &String::from_utf8_lossy(&lock_pristine)).await;
814838
stage_patch(&proj, &fx.orig, &fx.patched);
815839
let (code, stdout, stderr) = run_socket_env(
@@ -847,7 +871,7 @@ async fn classic_hosted_then_vendored_takeover_round_trips_to_registry() {
847871
"classic",
848872
false,
849873
&hosted_url,
850-
&lock_pristine,
874+
lock_upstream.as_bytes(),
851875
&pkg_json_pristine,
852876
&stdout,
853877
);
@@ -1055,10 +1079,10 @@ async fn classic_vendored_then_hosted_takeover_leaves_pure_hosted() {
10551079
)
10561080
});
10571081

1058-
// The originals chain across migrations: `rollback` restores the hosted
1059-
// pin's upstream registry entry, which is the pristine lock byte for
1060-
// byte (online: the entry is re-resolved from the registry document).
1061-
let registry =
1082+
// Rollback re-resolves the upstream registry entry. Hosted mode added an
1083+
// integrity line even on pre-1.10 yarn; v5 has no saved fragment to tell
1084+
// whether it was originally absent, so it restores the registry hash.
1085+
let (registry, lock_upstream) =
10621086
mount_registry_from_classic_lock(&server, &String::from_utf8_lossy(&lock_pristine)).await;
10631087
let (code, stdout, stderr) = run_socket_env(
10641088
&proj,
@@ -1076,8 +1100,27 @@ async fn classic_vendored_then_hosted_takeover_leaves_pure_hosted() {
10761100
assert_eq!(code, 0, "rollback failed: {stdout}\n{stderr}");
10771101
assert_eq!(
10781102
read(&proj, "yarn.lock"),
1079-
String::from_utf8_lossy(&lock_pristine),
1080-
"rollback lands on the pristine registry lock"
1103+
lock_upstream,
1104+
"rollback restores the registry lock, allowing the added upstream integrity"
1105+
);
1106+
let fresh = fresh_checkout(&proj, fx.tmp.path(), "classic-rollback", false);
1107+
let fresh_cache = fx.tmp.path().join("fresh-cache-classic-rollback");
1108+
let ci = corepack(
1109+
&fresh,
1110+
&yarn_classic_vex::yarn_classic(),
1111+
&["install", "--frozen-lockfile", "--no-progress"],
1112+
&[("YARN_CACHE_FOLDER", fresh_cache.to_str().unwrap())],
1113+
);
1114+
assert!(
1115+
ci.status.success(),
1116+
"fresh-checkout rollback install must succeed.\nstdout:\n{}\nstderr:\n{}",
1117+
String::from_utf8_lossy(&ci.stdout),
1118+
String::from_utf8_lossy(&ci.stderr),
1119+
);
1120+
assert_eq!(
1121+
std::fs::read(fresh.join("node_modules").join(DEP).join("index.js")).unwrap(),
1122+
fx.orig,
1123+
"rollback installs the pristine registry bytes"
10811124
);
10821125
}
10831126

‎docs/testing/vlt-compatibility.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ The capstones serve npmjs bytes from a local wiremock registry `R` and write
134134
| rc.30 … rc.32 | `{"config":{"registry": R}}` | yes | URL-segment DepIDs |
135135
| ≥ rc.33 | `{"config":{"registries":{"npm": R}}}` (+ `config.registry = R` for rc.33 … 1.0.4) | yes | |
136136

137+
The 0.0.0-1 and 0.0.0-11 writers can record an explicit npmjs tarball URL
138+
despite the configured harness registry. v5 rollback reconstructs the upstream
139+
pin without a saved lock fragment, so the rollback assertions allow that target
140+
URL to be omitted or restored on the harness registry. They still compare every
141+
other byte, including bystanders and CRLF line endings, and verify pristine
142+
package contents after a real install from the restored lock.
143+
137144
`scripts/backtest-vlt.py`'s `write_vlt_json` follows the same table against
138145
public npm (a `registry` equal to vlt's npmjs default is left out: vlt strips
139146
it from the lock anyway).

‎scripts/yarn-classic-vex-matrix.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ for release in "${releases[@]}"; do
4141
for entry in "${suites[@]}"; do
4242
IFS=: read -r suite filter ignored <<<"$entry"
4343
echo "== yarn@$release $suite ${filter:-}" >&2
44+
start_line=$(wc -l < "$log")
4445
if ! (cd "$root" && SOCKET_PATCH_YARN_CLASSIC_E2E_VERSION="$release" \
4546
SOCKET_PATCH_YARN_E2E_REQUIRED=1 \
4647
cargo test -q -p socket-patch-cli --test "$suite" -- ${filter:+"$filter"} \
4748
${ignored:+"$ignored"} --nocapture --test-threads=1 >>"$log" 2>&1); then
4849
echo "FAIL yarn@$release $suite (log: $log)" >&2
50+
tail -n "+$((start_line + 1))" "$log" >&2
4951
echo "VEXCELL leg=$suite yarn=$release mode=- cell=SUITE FAIL" >>"$log"
5052
status=1
5153
fi

0 commit comments

Comments
 (0)