The composer test's workspace replaces every module it lays beside at the zero version (issue 1921, issue 1966)

Go 1.27 looks the zero version of a workspace member up once a build loads the module graph; a versioned replace is the
form it takes with no lookup. workspaceOf writes the use list and those replaces, and a unit case loads such a graph with
GOPROXY=off. The only go.work the build path writes is this fixture's: the build module itself places requirements by
replaces in its modfile (pins.go), which are unversioned and not in a workspace, so they are untouched.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-06 10:00:17 +01:00
parent 21eae590e0
commit b02868f209
2 changed files with 81 additions and 1 deletions
@@ -172,7 +172,10 @@ func TestAComposeAskIsBuiltOnceAndAnsweredOnTheBus(t *testing.T) {
}
copyHostTree(t, hostTree, tree)
if len(beside) > 0 { // the group's other repositories, laid beside the node-engine's tree as one workspace
work := "go 1.26.0\n\nuse (\n\t" + tree + "\n\t" + strings.Join(beside, "\n\t") + "\n)\n"
work, err := workspaceOf(append([]string{tree}, beside...))
if err != nil {
return nil, err
}
if err := os.WriteFile(filepath.Join(m.Work, ws, "go.work"), []byte(work), 0o600); err != nil {
return nil, err
}
@@ -0,0 +1,77 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// zeroVersion is the version a mesh package is required at in every go.mod of the mesh (ADR 0573 §2, §4).
const zeroVersion = "v0.0.0-00010101000000-000000000000"
// workspaceOf is the go.work of the modules in dirs, as a test lays the group's repositories beside a tree: each used, and
// each also replaced at the zero version by its own directory. Go (from 1.27) looks the zero version of a required
// module up even where the module is a member of the workspace, once a build loads the module graph (any import outside
// the workspace); a versioned replace is the one form it takes without a lookup, and an unversioned one is refused for a
// member of the workspace (issue 1921: the composer's build fetched mesh-contracts from the network).
func workspaceOf(dirs []string) (string, error) {
var use, replace strings.Builder
seen := map[string]bool{}
for _, d := range dirs {
raw, err := os.ReadFile(filepath.Join(d, "go.mod"))
if err != nil {
return "", err
}
path := ""
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) == 2 && f[0] == "module" {
path = strings.Trim(f[1], `"`)
break
}
}
if path == "" {
return "", fmt.Errorf("%s/go.mod names no module", d)
}
if seen[path] {
continue // the same module laid twice: go refuses a workspace that uses it twice
}
seen[path] = true
fmt.Fprintf(&use, "\t%s\n", d)
fmt.Fprintf(&replace, "replace %s %s => %s\n", path, zeroVersion, d)
}
return "go 1.26.0\n\nuse (\n" + use.String() + ")\n\n" + replace.String(), nil
}
// Issue 1921 (the fourth group try): a workspace whose members require each other at the zero version loads its module
// graph with no lookup, so with nothing fetched (GOPROXY=off), as a composer built beside the group's heads does.
func TestAWorkspaceOfTheGroupResolvesTheZeroVersionWithNoLookup(t *testing.T) {
root := t.TempDir()
mod := func(name, body string) string {
dir := filepath.Join(root, name)
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
return dir
}
a := mod("a", "module git.novox.be/novox/mesh-host\n\ngo 1.26.0\n\nrequire git.novox.be/novox/mesh-contracts/go/x "+zeroVersion+"\n")
b := mod("b", "module git.novox.be/novox/mesh-contracts/go/x\n\ngo 1.26.0\n")
work, err := workspaceOf([]string{a, b, b})
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "go.work"), []byte(work), 0o600); err != nil {
t.Fatal(err)
}
cmd := exec.Command("go", "list", "-m", "all")
cmd.Dir = a
cmd.Env = append(os.Environ(), "GOWORK="+filepath.Join(root, "go.work"), "GOPROXY=off", "GOFLAGS=", "GOTOOLCHAIN=local")
if out, err := cmd.CombinedOutput(); err != nil || !strings.Contains(string(out), "git.novox.be/novox/mesh-contracts/go/x") {
t.Fatalf("the workspace's module graph does not load with nothing fetched: %v\n%s\n%s", err, out, work)
}
}