From 06c6909c1d366c300b679a98d1f86eaef047d052 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 01:56:48 +0000 Subject: [PATCH 1/2] Fold go.work use paths before resolving symlinks same_dir_key canonicalized a go.work `use` directory and fell back to lexical folding only when canonicalize failed. `./x/../app` with no `x` directory fails to canonicalize, so it kept the unresolved temp prefix while the project root resolved through symlinks. On macOS, where the temp dir lives under the /var -> /private/var symlink, the two never matched and go_work_that_does_not_use_the_project_makes_ the_replace_inert failed in every macOS `test` leg (full-scope, workflow_dispatch and nightly CI). Fold lexically first, as go's filepath.Join does, then resolve symlinks on the folded path. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AKt88ZvtcGWp1UvdvmTerp --- .../src/vendor/go_consumer_sync.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/go_consumer_sync.rs b/crates/socket-patch-core/src/vendor/go_consumer_sync.rs index ce7649ab9..f4c32a66a 100644 --- a/crates/socket-patch-core/src/vendor/go_consumer_sync.rs +++ b/crates/socket-patch-core/src/vendor/go_consumer_sync.rs @@ -463,12 +463,15 @@ async fn workspace_issues(project_root: &Path, replaces: &[ReplaceEntry]) -> Vec } /// A directory as go matches a module root against the workspace's `use` -/// list: absolute, with symlinks resolved when it exists, else lexically -/// normalized. +/// list: absolute and lexically normalized (go's `filepath.Join` folds +/// `x/../app` even when `x` does not exist), then with symlinks resolved +/// when it exists. Folding first keeps `./x/../app` comparable with the +/// project under a symlinked temp dir (macOS `/var` -> `/private/var`). fn same_dir_key(dir: &Path) -> PathBuf { - let abs = std::env::current_dir().unwrap_or_default().join(dir); - std::fs::canonicalize(&abs) - .unwrap_or_else(|_| crate::utils::relpath::normalize_lexically_keeping_escapes(&abs)) + let abs = crate::utils::relpath::normalize_lexically_keeping_escapes( + &std::env::current_dir().unwrap_or_default().join(dir), + ); + std::fs::canonicalize(&abs).unwrap_or(abs) } /// Where a replace sends the module, compared as go compares replacements From f37a41bbfac384fb43a65e3e28823bff5effce5c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 02:04:00 +0000 Subject: [PATCH 2/2] Resolve the project root before folding go.work Review follow-up. Folding every key lexically first also folded the project root, so a root spelled `link/../app` named a different directory than the one being patched. Canonicalize first as before, and fold-then-resolve only a path that fails to canonicalize. Add a unix test that reaches the workspace through a symlink, so Linux PR CI covers the macOS /var -> /private/var case. It fails on the previous implementation. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AKt88ZvtcGWp1UvdvmTerp --- .../src/vendor/go_consumer_sync.rs | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/go_consumer_sync.rs b/crates/socket-patch-core/src/vendor/go_consumer_sync.rs index f4c32a66a..cf28d93a2 100644 --- a/crates/socket-patch-core/src/vendor/go_consumer_sync.rs +++ b/crates/socket-patch-core/src/vendor/go_consumer_sync.rs @@ -463,15 +463,16 @@ async fn workspace_issues(project_root: &Path, replaces: &[ReplaceEntry]) -> Vec } /// A directory as go matches a module root against the workspace's `use` -/// list: absolute and lexically normalized (go's `filepath.Join` folds -/// `x/../app` even when `x` does not exist), then with symlinks resolved -/// when it exists. Folding first keeps `./x/../app` comparable with the -/// project under a symlinked temp dir (macOS `/var` -> `/private/var`). +/// list: absolute, with symlinks resolved when it exists. A path that only +/// exists once folded (`./x/../app` with no `x`, which go's `filepath.Join` +/// folds) is folded lexically and then resolved, so it still matches a +/// project under a symlinked dir (macOS `/var` -> `/private/var`). fn same_dir_key(dir: &Path) -> PathBuf { - let abs = crate::utils::relpath::normalize_lexically_keeping_escapes( - &std::env::current_dir().unwrap_or_default().join(dir), - ); - std::fs::canonicalize(&abs).unwrap_or(abs) + let abs = std::env::current_dir().unwrap_or_default().join(dir); + std::fs::canonicalize(&abs).unwrap_or_else(|_| { + let folded = crate::utils::relpath::normalize_lexically_keeping_escapes(&abs); + std::fs::canonicalize(&folded).unwrap_or(folded) + }) } /// Where a replace sends the module, compared as go compares replacements @@ -927,6 +928,26 @@ mod tests { } } + /// A workspace reached through a symlink (macOS temp dirs live under + /// `/var` -> `/private/var`) still counts the project as a member when + /// the `use` path only exists once folded (`./x/../app`, no `x`). + #[cfg(unix)] + #[tokio::test] + async fn go_work_member_matches_through_a_symlinked_workspace() { + let dir = tempfile::tempdir().unwrap(); + let real = dir.path().join("real"); + std::fs::create_dir(&real).unwrap(); + let ws = dir.path().join("link"); + std::os::unix::fs::symlink(&real, &ws).unwrap(); + let root = ws.join("app"); + consumer(&root, REPLACE); + let none = HashMap::new(); + for use_dir in ["./app", "./x/../app"] { + write(&ws, "go.work", &format!("go 1.21\n\nuse {use_dir}\n")); + assert_eq!(audit(&root, &none).await, vec![], "{use_dir}"); + } + } + /// #531: two members replacing one module@version with different /// directories make go refuse the workspace. The same hosted module in /// both, or the same directory, is fine, and so is a replace of