Skip to content

Test Go workspace vendoring and GOWORK - #1765

Merged
Mikola Lysenko (mikolalysenko) merged 33 commits into
mainfrom
test/golang-workspace-e2e
Oct 28, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 33 commits into
mainfrom
test/golang-workspace-e2e

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Stacked on the go.work writer PR. Tests only.

Change

Real-go e2e coverage for Go workspaces in crates/socket-patch-cli/tests/e2e_golang_workspace_build.rs:

  • go work vendor in vendored and hosted modes (Go ≥ 1.22);
  • which file Go writes checksums to when it runs in a workspace;
  • GOWORK set to the same go.work by absolute path, or to a go.work elsewhere;
  • idempotent re-runs.

The fixtures fill the module cache first, because workspace selection now reads dependency go.mod files from it.

Validation

go1.26.3: e2e_golang_workspace_build 30/0. Every other Go suite is unchanged from the writer PR (all passing).

🤖 Generated with Claude Code


Note

Low Risk
Test-only changes to the Go workspace e2e suite; no production code paths modified.

Overview
Adds real-go e2e tests only in e2e_golang_workspace_build.rs (stacked on go.work writer behavior).

go_work_vendor_records_the_go_work_replace (Go ≥ 1.22): With a committed workspace vendor/, after vendored or hosted patches add a go.work replace, socket-patch emits go_vendor_modules_txt_out_of_sync, -mod=vendor builds fail until go work vendor updates vendor/modules.txt; then both members build PATCHED offline, and vendored mode passes vendor --check.

run_location_and_gowork_decide_the_replace_file: vendor from a member writes that member's go.mod; a second member gets a conflicting replacements error. From the workspace root (or GOWORK pointing at the same go.work), the shared module is replaced once in go.work and both members build patched. GOWORK pointing at a different go.work leaves the root with nowhere to write and fails without mutating go.work.

Also adds a work_vendor helper wrapping go work vendor.

Reviewed by Cursor Bugbot for commit 4a0e1fe. Configure here.


Generated by Claude Code

When nothing imports a package from a hosted-patched Go module (it is
only a transitive requirement), `go mod tidy` keeps just the socket
module's `/go.mod` go.sum line and drops its zip line. The build still
succeeds offline, but VEX discovery required both lines, so the patch
was skipped as `package_not_found` instead of attested.

The `/go.mod` line for the exact socket module+version is now enough to
recognise the pin. Without the zip line the ref has no locked integrity
and is not integrity-required, so it attests from the lockfile: go
compiles none of the module's packages, and the module path carries
the patch uuid. A zip line, when present, must still be the single
well-formed hash and supplies `locked_integrity`. Conflicting hashes or
a malformed line for the socket module+version still mean no pin.

The hosted writer is unchanged: it still writes both lines and refuses
a module outside the build graph.

Assisted-by: Claude Code:claude-opus-5-5
A `/go.mod`-only go.sum line also exists for versions MVS reads but
does not select, so in a go < 1.17 graph a `/go.mod`-only hosted pin
whose version go.sum shows is not selected is now reported as inert
instead of attested.

In workspace mode go verifies against go.work.sum and every member's
go.sum, so discovery now reads those too (in-root members only) and a
member's zip line still supplies the locked integrity.

go.sum parsing now follows go: a line of other than three tokens makes
the whole file unusable, a non-dirhash `h1:` breaks that module's pin,
and hashes of other algorithms are ignored.

Re-running the hosted rewrite over a project `go mod tidy` pruned to
the `/go.mod` line now leaves go.sum alone instead of re-adding the zip
line the next tidy removes again.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
In a go.work workspace, Go builds the highest version any `use` member
requires. A Socket replace whose go.sum held only the `/go.mod` line
was still attested when another member required a newer version, so
VEX could claim a patch that no build links. Such a pin now attests
only when every member's go.mod is inside the project and readable and
together they select the replaced version; otherwise it is flagged as
inert or left unattested.

Pins are also looked up only in the sum files Go actually reads: in a
workspace, go.work.sum and the members' go.sum (the root go.sum only
when `.` is a member); with GOWORK=off, the root go.sum alone, and the
go.work replaces are ignored as Go ignores them.

Assisted-by: Claude Code:claude-opus-5-5
`go mod tidy` drops the Socket module's zip checksum line while no
package of the patched module is imported. If the project later
imports it, the build fails with "missing go.sum entry", and
re-running `scan --mode hosted` left that state untouched, so the
project had no supported way back. A re-run now always writes both
go.sum lines, so it repairs the project and the build uses the patched
module again. The trade-off is that the next `go mod tidy` may drop
the line again while the module stays unimported.

Assisted-by: Claude Code:claude-opus-5-5
A go.work build never makes a member's go.mod list the versions it
builds, and it adds missing checksums to go.work.sum on its own. So a
member's dependency can pull in a newer version of a patched module
than any committed file names, and the build links the unpatched
module. vex used to attest such a pin from the lockfile whenever the
members' go.mod files named the patched version.

In a workspace a pin with only the /go.mod line now needs a verifying
installed copy, and a newer version in a member's require or in any
recorded zip line marks the replace as inert. Docs and the CLI contract
say so, and a real-go test covers the member dependency case.

Assisted-by: Claude Code:claude-opus-5-5
go only ever appends to go.work.sum: once a member stops depending on
whatever raised a module, the zip line of the higher version stays
behind. vex treated such a line as proof the workspace builds that
version, so it dropped a Go /go.mod-only pin as inert even when go
builds the patched version.

Only a member's require or the zip lines of a member's go.sum now mark
a workspace pin inert. The pin still needs a verifying installed copy
to attest.

Assisted-by: Claude Code:claude-opus-5-5
A workspace's sum files are only appended to, so the socket zip line a
past build wrote to go.work.sum (or a member go.sum) stays after a member
starts requiring a higher version of the replaced module. Go then builds
that higher version and the replace is inert, but discovery attested the
zip-pinned ref from the lockfile anyway, and the CLI's post-gate reads
only the root go.mod and go.sum, so vex claimed a patch the build never
links. Run the same inert check GoModOnly pins already get on Zip pins
in workspace mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each Go command checked go.work its own way, and some of those checks
disagreed with go. A replace pinned to v1.0.0 counted as patched even
when another workspace member required v1.1.0, so go built the newer,
unpatched version while apply, vendor and vex treated it as fixed. A
go.work replace of a different version was taken to override a
member's replace, though go still uses the member's. GOWORK=auto was
treated as a file name, and a relative GOWORK, which go rejects, was
read as if it were in effect.

Every Go path now asks one workspace model. It finds go.work as go
does, picks the version the whole workspace builds from all members'
requirements, and applies go's replace precedence. A member go.mod
that can't be read leaves the answer unknown, so nothing is attested
on a guess. VEX now also reads member go.mod files from a go.work root
that has no go.mod of its own, and apply and vendor refuse there and
name the members to run them in.

Assisted-by: Claude Code:claude-opus-5-5
`scan --mode hosted` wrote a Go replace without looking at the go.work
in effect, and only warned afterwards. A replace that conflicted with
another member's broke every build in the workspace, and one that
go.work overrode was reported as redirected while go built something
else.

Hosted mode now checks the workspace before writing. A replace go.work
would override, ignore or reject is not written, the run warns
`redirect_golang_workspace_conflict`, and the patch is not reported
as redirected. The version check uses the whole workspace too, so a
member that doesn't require the module can still carry the replace
when another member does, and one that raises it to a newer version
refuses the pin. From a go.work root with no go.mod of its own, the
pin goes into each member that lists the module.

Assisted-by: Claude Code:claude-opus-5-5
`rollback` only looked at the root go.mod and go.sum. A hosted pin
in go.work was refused with a "restore it from version control"
error, the Socket module's lines that go writes to go.work.sum in
workspace mode were left behind, and a pin in a workspace member's
go.mod was not found from the workspace root.

The restore now edits the go.mod or go.work each pin was found in,
removes the Socket module's lines from that file's go.sum,
go.work.sum and, for a go.work pin, the members' go.sum, and puts
the upstream module's lines back where the Socket ones were.

Assisted-by: Claude Code:claude-opus-5-5
Real-go tests now cover a go.work root without a go.mod (hosted pins
in each member, attested from the root, rolled back byte for byte),
a member raising the patched module's version, GOWORK unset, `auto`,
`off` and relative, the hosted go.work gate under go's replace
precedence, and rollback of a go.work pin and its go.work.sum lines.
Each test fails before this branch's fixes.

The CLI contract and the Go ecosystem docs now describe how the
workspace is found, how its version and replace precedence are
read, and what each command does from a go.work root.

Assisted-by: Claude Code:claude-opus-5-5
In a go.work workspace go keeps no member's go.mod tidy, so a member's
dependency can raise a patched module above the version the Socket
replace is pinned to; the only trace is the zip line go writes to
go.work.sum or a member's go.sum. A higher zip line now proves the pin
inert even when a member's go.mod is out of reach, for vex and for
apply, vendor and hosted writes.

Another member replacing every version of the module (replace M =>
...) now blocks apply, vendor and hosted writes and makes vex leave the
patch out: go links that target or refuses to build.

At a go.work root, a hosted pin lands in every member that lists the
module or in none, instead of staying half-written when one member
refuses.

When GOWORK names a file other than the project's go.work, or a
relative path go rejects, vex attests no Go patch and reports each one.

Assisted-by: Claude Code:claude-opus-5-5
In a go.work workspace three things made socket-patch report a Go patch
as applied, or refuse one, when go did otherwise:

- A module that is itself a `use` member (a local checkout) is built
  from its directory, and go ignores every replace of it. apply, vex and
  hosted mode still wrote and attested the replace.
- go only ever appends to go.work.sum, so a zip line of a version that
  nothing requires any more stayed behind and made apply and hosted mode
  refuse a pin that go builds.
- A member's dependency can raise the module past every member require.
  Before a build wrote go.work.sum nothing showed it, so a fresh clone
  attested a pin that go ignores.

The selection is now worked out the way go does it: minimal version
selection over each member's requires and the go.mod of every
dependency go loads, read from its replacement directory or the module
cache. Sum files are no longer evidence. When a needed go.mod is missing
the answer is unknown: apply --check fails, vex leaves the patch out
(go_version_not_selected) and hosted mode writes nothing, until
`go mod download` fills the cache. A replace of a workspace module is
refused as a workspace conflict.

Assisted-by: Claude Code:claude-opus-5-5
The repo guard forbids private --json envelope readers in test files,
so the workspace e2e reads warning codes through the shared helper.

Assisted-by: Claude Code:claude-opus-5-5
With the workspace model, the go.work-level check reports a raised
zip-pinned replace as inert (the workspace requires the higher
version), so the test asserts the verdict and version, not the
module-mode sentence.

Assisted-by: Claude Code:claude-opus-5-5
Run in the directory holding go.work, apply, vendor and hosted scans
now replace a module the workspace shares once in go.work, the way
`go work edit -replace` does: when that directory has no go.mod of
its own, when another member lists the module, or when go.work already
holds our replace of it. Before, member replaces of one patched copy
resolved to different directories and go refused the workspace, so
the second member was refused and the docs told users to move the
replace by hand; a go.work root without a go.mod could not be patched
by apply or vendor at all.

The vendored copy stays at the root and the go.work target is relative
to it. A hosted pin is version-specific and its sum lines go into
go.work.sum, leaving the members' go.mod and go.sum untouched. A
go.work replace is refused when go.work already replaces that version
with something else, or a member's own replace would be overridden.

The vendored ledger records go.work as the wiring file, and the
project's replaces are read from both files, so --check, remove,
vendor --revert, list and vex find and restore them there. Rollback
of a go.work pin no longer adds upstream sum lines that were never
removed, and deletes a go.work.sum the pin alone created, so go.work
and go.work.sum come back byte for byte.

Assisted-by: Claude Code:claude-opus-5-5
Describe when each Go mode writes its replace into go.work, how the
target and go.work.sum pin look, what a GOWORK=off build sees, and how
check, revert, vex and rollback treat it. Drop the advice to move the
replace into go.work by hand.

Assisted-by: Claude Code:claude-opus-5-5
Real-go e2e from a go.work root with no go.mod, two members requiring
the same module: vendor, apply and hosted each write one replace, both
members build the patched module with -mod=readonly and go list -m all
shows the replacement, a re-run changes nothing, and vendor --revert
and rollback give go.work back byte for byte.

Assisted-by: Claude Code:claude-opus-5-5
A hosted scan that pins a module in go.work now checks a committed
workspace vendor/modules.txt too, and names `go work vendor` when it
does not record the new replace, as it already did for go.mod pins.

Assisted-by: Claude Code:claude-opus-5-5
A replace that apply wrote into the root go.mod, before another
workspace member required the module, now moves into go.work the next
time apply or vendor writes it. Before, vendor added the go.work line,
left the go.mod line behind and then deleted the copy it named, so a
GOWORK=off build of the root failed and vendor --revert kept the
dangling line. The ledger now records that original against go.mod.

Rolling back hosted pins of one patch held in several members' go.mod
and in go.work now puts the upstream go.sum lines back in every
member's go.sum and deletes the go.work.sum the pin created. Before,
only the first member got its lines back, so the others failed
-mod=readonly builds, and go.work.sum was left holding upstream lines.

apply, vendor and hosted mode again refuse a project whose go.mod the
go.work beside it does not use, instead of writing a go.work replace
that patches the other members but never the project itself.

Assisted-by: Claude Code:claude-opus-5-5
Run at a go.work root without a go.mod of its own, apply, vendor and
hosted mode write a shared replace into go.work. When the replaced
module is itself a `use` member (a local checkout), go builds it from
that directory and ignores the replace, so the patch was reported as
applied while go linked the local, unpatched copy.

The go.work check now refuses that replace, as the member go.mod check
already does. The e2e test runs hosted mode from the root as well, and
the shared-module re-run test fills the module cache first, since a
workspace's selection is read from it.

Assisted-by: Claude Code:claude-opus-5-5
apply writes a shared replace into go.work, and apply, remove and
rollback rewrite go.mod, with an atomic rename. When either file was a
symbolic link the rename replaced the link with a regular file, so the
user's link was gone for good, while vendor and hosted mode refused the
same layout and wrote nothing.

apply now refuses before it copies anything when the file that would
hold the replace is a link, and every go.mod or go.work edit refuses
to change a symlinked file, so remove and rollback leave it alone too.

Assisted-by: Claude Code:claude-opus-5-5
A replace appended to a go.mod or go.work that had no final newline
ended with one, and remove, vendor --revert and rollback had no way to
tell it was added. The file then kept an extra newline after a full
revert, so the tree showed a diff the docs say it won't.

The appended replace now ends without a newline when the file did, and
removing it restores the file byte for byte. The hosted rewriter's
golden digests change for the seeded go.mod files without one.

Assisted-by: Claude Code:claude-opus-5-5
The docs said rollback deletes a go.work.sum "the pin alone created".
Hosted mode keeps no record of which files existed before a pin, so it
deletes any go.work.sum that stripping the Socket lines leaves empty,
including one a user created empty. go never writes an empty
go.work.sum, so that only happens to a file made empty by hand. Say
so, rather than claim rollback can tell the cases apart.

Assisted-by: Claude Code:claude-opus-5-5
Real-go e2e for two more workspace cases. A committed workspace
vendor/ directory: after vendor or hosted mode pins the shared module
in go.work, the run names `go work vendor`, the -mod=vendor build
fails until it is re-run, then both members build the patched module
offline. Where the run happens: a member run writes its own go.mod and
a second member is pointed at the workspace root, the root run writes
go.work, GOWORK naming the same file by absolute path behaves the same,
and GOWORK naming a go.work elsewhere refuses and writes nothing.

Assisted-by: Claude Code:claude-opus-5-5
The format registry now lists go.work and go.work.sum as hosted Go
files, so the by-value candidate pin includes them.

Assisted-by: Claude Code:claude-opus-5-5
The go work vendor test pins a shared module with hosted mode from the
workspace root on a fresh machine. Hosted mode reads which version the
workspace selects from the module graph in the module cache and writes
nothing while it is empty, so the test now downloads the module first,
as an earlier build would have.

Assisted-by: Claude Code:claude-opus-5-5
The VEX selection gate only read Socket replaces from each module
root's go.mod, so a replace written into go.work itself skipped it and
was attested on a fresh clone with no module graph cached, contradicting
golang_workspace_without_its_module_graph_is_not_attested. Feed the
workspace's own replaces through the same gate (Bugbot, vex.rs:1006).

The go.work fixture now caches the module graph, as `go mod download`
would, since two existing go.work tests relied on attesting with an
empty module cache.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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.

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: labeled Ready for review at head 5770cd3.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. This PR only adds tests. It is stacked on #1764 (the go.work writer), which is stacked on #1763 and #1748. It adds two real-go e2e tests for Go workspaces:

  • go work vendor in vendored and hosted modes: the out-of-sync warning, the -mod=vendor build failing as Go says, then a clean offline build after re-vendoring.
  • Where the replace is written when running from a member versus the root, and how GOWORK (same file by absolute path, or a go.work elsewhere) changes that.

Risk: low. No production code changes. One gap: the new tests run only in go-compatibility.yml, and that workflow does not appear among this head's checks, so CI has not run them on this head.

Look here

  • e2e_golang_workspace_build.rs:1444 go_work_vendor_records_the_go_work_replace
  • e2e_golang_workspace_build.rs:1537 run_location_and_gowork_decide_the_replace_file, including the GOWORK-elsewhere refusal that writes nothing

Verified

  • I read the full diff against the base feat/golang-workspace-writer (+190, 1 file).
  • Locally with go1.24.7: cargo test -p socket-patch-cli --all-features --test e2e_golang_workspace_build passed 30/30 in 195s, with a real toolchain, so the tests did not early-return.
  • Repo CI on the head: 30/30 passed. Bugbot is clean. CHANGELOG.md is untouched.

Changes I made: none.

Open questions (not blocking):

  1. The description also mentions checksum-file and idempotency coverage. Those tests come from the base branch, not this PR's diff.
  2. Merge order: this targets feat/golang-workspace-writer, not main, so I did not arm auto-merge. It should land after Write shared Go patch replaces into go.work #1764, and Write shared Go patch replaces into go.work #1764's own base chain goes through Keep Go hosted pins after go mod tidy drops the zip line #1748, which is agent:needs-human.

Generated by Claude Code

Base automatically changed from feat/golang-workspace-writer to main October 28, 2026 21:56
@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 28, 2026
#1764, which this PR stacked on, squash-merged into main as eb75d20, so
every conflict was between this branch's older copy of that stack and
the landed version. Take main's side for all of them and keep this PR's
only unique change: the two go.work tests in
e2e_golang_workspace_build.rs (go work vendor, and which file a run
location writes the replace to). The full e2e_golang_workspace_build
target passes locally (30/30).

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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 4a0e1fe. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: re-labeled Ready for review at head 4a0e1fe.

  • Merged main to clear the conflicts. Write shared Go patch replaces into go.work #1764, which this PR stacked on, squash-merged as eb75d20, so every conflict was between this branch's older copy of that stack and the landed code. I took main's side throughout. The PR diff against main is now just the 190 test lines in e2e_golang_workspace_build.rs (go_work_vendor_records_the_go_work_replace, run_location_and_gowork_decide_the_replace_file).
  • CI: 50/50 check runs on 4a0e1fe passed or skipped (31 success, 19 skipped). No main-wide failures.
  • Bugbot: reviewed 4a0e1fe, no findings.
  • Locally: cargo test -p socket-patch-cli --test e2e_golang_workspace_build passed 30/30 with Go installed.
  • For reviewers: the PR description still describes the stack on Write shared Go patch replaces into go.work #1764. Only the two new go.work tests remain to review.

Generated by Claude Code

Merged via the queue into main with commit ecd4924 Oct 28, 2026
50 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the test/golang-workspace-e2e branch October 28, 2026 22:56
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 28, 2026
Resolve conflicts with main's go.work replace hosting (#1763, #1764,
#1765). ensure_replace_entry now validates and backs up go.sum before
writing the replace into main's replace host (go.work or go.mod), and
drop_replace_entry restores the saved go.sum lines before removing the
replace from go.work and go.mod. The cleanup fallback uses the same
go.work-aware removal now that edit_go_mod is gone. The workspace e2e
test keeps this branch's reinstall steps on main's tail(&Tail{..}) shape.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants