Skip to content

Commit 75b6f65

Browse files
committed
Read activestorage's own block in hosted gem leg
Production now publishes patches for activestorage's dependencies (actionpack, activesupport 6.0.3), so the hosted Gemfile rewrite can hold several patch-registry blocks. wired_gem_registry_uuid took the first one, so the leg checked actionpack's UUID against GEM_UUIDS and failed with a misleading "extend GEM_UUIDS" message whenever a transitive block came first or activestorage was not wired at all (hosted-e2e, 2026-11-01, runs 41330638545 and 41330161127). Match the block whose body declares gem "activestorage". When activestorage is not wired, the leg now says so instead of naming a neighbour's patch. Add a unit test for the parser. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011f9aeRg9Pkq38o8d4ttAvb
1 parent cbbbdb9 commit 75b6f65

1 file changed

Lines changed: 67 additions & 11 deletions

File tree

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

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -707,17 +707,72 @@ fn gem_lock_checksum(lock: &str, name: &str, version: &str) -> Option<String> {
707707
}
708708

709709
/// The patch UUID the gem rewriter wired into `Gemfile`'s per-dep Socket
710-
/// source block. The block's URL shape is
710+
/// source block for `name`. The block's URL shape is
711711
/// `https://<PATCH_HOST>/patch-registry/gem/<grant>/<patch-uuid>/`; the grant
712712
/// token is itself UUID-shaped, so this takes the SECOND path segment rather
713713
/// than scanning for "something UUID-like".
714-
fn wired_gem_registry_uuid(gemfile: &str) -> Option<String> {
715-
let marker = format!("https://{PATCH_HOST}/patch-registry/gem/");
716-
let rest = &gemfile[gemfile.find(&marker)? + marker.len()..];
717-
let path = &rest[..rest.find('"')?];
718-
let mut segments = path.split('/').filter(|s| !s.is_empty());
719-
let _grant = segments.next()?;
720-
segments.next().map(str::to_string)
714+
///
715+
/// The rewriter writes one block per redirected gem, and production also
716+
/// publishes patches for `activestorage`'s own dependencies (`actionpack`,
717+
/// `activesupport`), so the Gemfile can hold several blocks in any order.
718+
/// Only the block whose body declares `gem "<name>"` counts: reading the
719+
/// first block would attribute a transitive gem's patch to `name`.
720+
fn wired_gem_registry_uuid(gemfile: &str, name: &str) -> Option<String> {
721+
let marker = format!("source \"https://{PATCH_HOST}/patch-registry/gem/");
722+
let decl = format!("gem \"{name}\"");
723+
let mut text = gemfile;
724+
while let Some(at) = text.find(&marker) {
725+
let rest = &text[at + marker.len()..];
726+
let path = &rest[..rest.find('"')?];
727+
// The block body runs from the opener's line to its `end` line.
728+
let body_start = rest.find('\n').map_or(rest.len(), |i| i + 1);
729+
let body = &rest[body_start..];
730+
let body = &body[..body.find("\nend").map_or(body.len(), |i| i + 1)];
731+
if body.lines().any(|l| l.trim_start().starts_with(&decl)) {
732+
let mut segments = path.split('/').filter(|s| !s.is_empty());
733+
let _grant = segments.next()?;
734+
return segments.next().map(str::to_string);
735+
}
736+
text = rest;
737+
}
738+
None
739+
}
740+
741+
#[test]
742+
fn wired_gem_registry_uuid_reads_the_named_gems_block() {
743+
let block = |grant: &str, uuid: &str, gem: &str| {
744+
format!(
745+
"source \"https://{PATCH_HOST}/patch-registry/gem/{grant}/{uuid}/\" do\n \
746+
gem \"{gem}\", \"6.0.3\"\nend\n"
747+
)
748+
};
749+
let transitive_first = format!(
750+
"source \"https://rubygems.org\"\n\n{}\n{}\n{}",
751+
block("g1", "u-actionpack", "actionpack"),
752+
block("g2", "u-activestorage", "activestorage"),
753+
block("g3", "u-activesupport", "activesupport"),
754+
);
755+
assert_eq!(
756+
wired_gem_registry_uuid(&transitive_first, "activestorage").as_deref(),
757+
Some("u-activestorage")
758+
);
759+
assert_eq!(
760+
wired_gem_registry_uuid(&transitive_first, "actionpack").as_deref(),
761+
Some("u-actionpack")
762+
);
763+
// `activestorage` left unwired beside wired transitive gems: no block
764+
// for it, never a neighbour's UUID (the 2026-11-01 hosted-e2e shape).
765+
let unwired = format!(
766+
"source \"https://rubygems.org\"\ngem \"activestorage\", \"6.0.3\"\n\n{}\n{}",
767+
block("g1", "u-actionpack", "actionpack"),
768+
block("g3", "u-activesupport", "activesupport"),
769+
);
770+
assert_eq!(wired_gem_registry_uuid(&unwired, "activestorage"), None);
771+
// A prefix of another gem's name is not a match.
772+
assert_eq!(
773+
wired_gem_registry_uuid(&block("g", "u", "activestorage-x"), "activestorage"),
774+
None
775+
);
721776
}
722777

723778
/// Locate the bundler-installed `gems/<name>-<version>` directory under a
@@ -2193,10 +2248,11 @@ async fn gem_bundler_hosted_install_proof() {
21932248
// pinned any-of set and then content-verifies against the one bundler
21942249
// was actually given.
21952250
let gemfile = read(&proj.join("Gemfile"));
2196-
let wired_uuid = wired_gem_registry_uuid(&gemfile).unwrap_or_else(|| {
2251+
let wired_uuid = wired_gem_registry_uuid(&gemfile, GEM_NAME).unwrap_or_else(|| {
21972252
panic!(
2198-
"{LEG}: Gemfile carries no per-dep Socket source block \
2199-
(`source \"https://{PATCH_HOST}/patch-registry/gem/<grant>/<uuid>/\" do`):\n{gemfile}"
2253+
"{LEG}: Gemfile carries no per-dep Socket source block declaring \
2254+
`gem \"{GEM_NAME}\"` (`source \"https://{PATCH_HOST}/patch-registry/gem/<grant>/<uuid>/\" do`) \
2255+
— the hosted scan did not redirect {GEM_NAME} itself:\n{gemfile}"
22002256
)
22012257
});
22022258
assert!(

0 commit comments

Comments
 (0)