Skip to content

Fix Go replaces that go.work ignores (#393, #458, #531) - #1393

Merged
Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
agent/fix-go-work-blind-redirect
Oct 11, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
agent/fix-go-work-blind-redirect

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #393
Fixes #458
Fixes #531

Summary

In a Go workspace, apply and vendor no longer write a go.mod replace
that the go.work in effect would override, ignore or reject. apply --check
and vendor --check now fail on such a replace that was written earlier, and
vex no longer attests the patch as not_affected.

Shared root cause

Every Go mode wires a patch by writing a replace into the project's own
go.mod. Every check (apply --check, vendor --check, VEX) then treated
that line as effective, and none of them asked which go.work the go command
uses. In workspace mode Go:

In all three cases socket-patch exited 0, --check said "in sync" and VEX
attested not_affected.

Fix (one shared boundary)

vendor::go_consumer_sync already knew how to find the workspace for its
vendor/modules.txt audit. It now:

  • loads the go.work in effect once: GOWORK from the environment or from
    go env -w, otherwise the nearest go.work at or above the project. It
    reads go.work's replaces, its use members and each member's replaces;
  • gets workspace_conflict / replace_refusal, which decide which of the
    three cases applies to a module@version replace, matching go's own rules:
    • any go.work replace of the module path drops every member replace, unless
      go.work's own replace for this version sends it to the project's copy;
    • membership is compared on resolved directories;
    • targets are compared lexically, as go compares replacements;
    • a hosted replace names the same Socket module in every member, so it
      never conflicts.

Callers:

  • Agent apply (golang_local::apply_go_redirect) and vendor
    (vendor::golang::vendor_go_module, new code vendor_go_workspace_conflict)
    refuse before copying or fetching anything. Dry runs and re-runs over an
    inert replace therefore refuse too, and no orphan copy is left.
    go_mod_edit::ensure_replace_entry refuses as well, as a second guard.
  • audit reports a replace that was already written as
    go_workspace_replace_ineffective. As a result apply --check and
    vendor --check fail, and apply, vendor and scan --mode hosted warn.
  • vex leaves the patch out with the new omission tag
    go_workspace_override, plus the warning.

Docs: docs/ecosystems.md (new "Go workspaces (go.work)" section) and the
golang entry in CLI_CONTRACT.md. No wrapper changes: the npm, pypi and gem
wrappers only dispatch to the binary.

Per-issue tests (red on main → green)

The three e2e tests below use the real go toolchain with a hermetic file
GOPROXY. On main (fix stashed) all three fail at the first assertion: apply
exits 0. With the fix they pass.

Commands run

  • cargo fmt --all -- --check: ok
  • cargo clippy --workspace --all-features -- -D warnings: ok
  • cargo test -p socket-patch-core --all-features: 6276 passed. The 4 that failed are read-only-permission tests that can't fail as root in this sandbox (copy_tree, vlt_heal, pypi_poetry, pypi_requirements); they're unrelated to Go. A full cargo test --workspace --all-features build ran out of the sandbox's disk allowance, so CI runs the rest
  • cargo test -p socket-patch-cli --all-features --lib --bins: 911 passed
  • cargo test -p socket-patch-cli --all-features --test e2e_vex_lockfile --test covgap_commands_vex --test e2e_vex_vendor --test e2e_vex_redirect --test covgap_commands_rollback: all pass
  • SOCKET_PATCH_GO_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_golang_build --test e2e_golang_workspace_build --test e2e_vendor_golang_build --test e2e_golang_hosted_build --test e2e_golang: all pass (go 1.24.7)

/code-review high findings

Fixed in 1413d3c:

  • The go.work exemption was keyed on socket_owned; it now compares the
    actual target.
  • When go.work's own replace was exempted, member checks still ran.
  • The check ran after the copy, which skipped dry runs and left orphan
    copies.
  • vendor re-runs used a misleading error code.
  • go env -w GOWORK was ignored. Bugbot then pointed out that my own env-file reader mishandled an empty GOWORK and skipped the regular-file guard; e91342d switches to the shared utils::go_env.
  • Each replace caused another read of the workspace (N×M reads), duplicating
    workspace_replaces.
  • Symlink canonicalisation of targets didn't match go's lexical comparison.

Not fixed: the hosted writer doesn't refuse. scan --mode hosted rewrites
through the pure, in-memory rewrite_golang, which only sees project-relative
file contents and can't look at an ancestor or GOWORK go.work. Hosted mode
is covered by the post-write warning, the --check failure and the VEX
omission, so it never makes a false attestation. A write-time refusal there
needs the go.work context passed into the rewriter. That's left as a
follow-up rather than widening this PR. For #531 hosted mode was never
affected: identical module replaces don't conflict.

🤖 Generated with Claude Code

https://claude.ai/code/session_018LqNhpS1viyHzW6Jmp3Wuq


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
In Go workspace mode a project go.mod replace does nothing when the
go.work in effect replaces the same module itself (#393) or doesn't
`use` the project (#458), and it breaks every workspace build when
another member replaces the same module@version elsewhere (#531).
socket-patch wrote the replace anyway and its checks called it in
sync, so the build stayed unpatched (or unbuildable) while apply,
vendor and their --check passes all reported success.

A new go_consumer_sync::workspace_conflict finds the go.work go would
use (GOWORK, else the nearest one above the project) and says which of
the three cases applies. ensure_replace_entry, which writes the replace
for agent apply and vendor, now refuses such a replace and names the
fix. audit reports one already written as
go_workspace_replace_ineffective, so apply --check and vendor --check
fail and apply, vendor and hosted scan warn.

Assisted-by: Claude Code:claude-opus-5-5
vex attested not_affected for a Go patch whose go.mod replace the
go.work in effect overrides, ignores or makes conflict, although the
build links the user's target, the unpatched module, or nothing at
all. It now omits such a patch with go_workspace_override and warns
with go_workspace_replace_ineffective.

Adds real-go regression tests for #393, #458 and #531: apply refuses,
and a replace written before the go.work appeared fails apply --check
and is left out of VEX.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups on the go.work check:

- apply and vendor now check before copying or fetching anything, so
  apply --dry-run predicts the refusal, a re-run over an inert replace
  fails instead of reporting it in sync, and a refused apply leaves no
  orphan copy under .socket/go-patches. vendor refuses with its own
  vendor_go_workspace_conflict code.
- A go.work replace counts as ours only when it sends this version to
  the project's own copy, however the path is spelled; when it does,
  other members' replaces are ignored, as go ignores them.
- Replacement targets are compared lexically, as go compares them, and
  GOWORK set with `go env -w` is honoured.
- The workspace is read once per audit and shared with the
  modules.txt check.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 11, 2026 17:41
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/vendor/go_consumer_sync.rs Outdated
Comment thread crates/socket-patch-core/src/vendor/go_consumer_sync.rs Outdated
The go.work check read GOWORK with its own env-file reader, which
skipped the `go env -w` file when GOWORK was set but empty and opened
the file without the regular-file guard. Use utils::go_env, which
resolves settings exactly as go does and reads the file safely.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e91342d. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 11, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at e91342d.

  • CI: all checks green on the head (35 success, rest skipped); no main-wide failures.
  • Bugbot: reviewed e91342d, no findings (the two earlier go_consumer_sync threads are resolved and outdated).
  • Mergeable with main (41659c1), no CHANGELOG.md change.

Worth a look: Go replace writers and checks now consult the go.work in effect (including GOWORK from go env), so a replace that go.work overrides or ignores no longer counts as applied.


Generated by Claude Code

Merged via the queue into main with commit e74a845 Oct 11, 2026
53 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-go-work-blind-redirect branch October 11, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment