DESIGN — the converted host’s package ancestry view

Status: implemented, lane claude/synthetic-goroot-class, 2026-08-13. Closes: the board’s converted-host WORKING-DIRECTORY class for five of its six members. Supersedes: the board’s “the remedy really is the full synthetic-GOROOT staging” ruling (BOARD-next-validation-candidates.md §The converted-host WORKING-DIRECTORY class — why no cheap subset exists), which was right that no cheap per-package subset exists and wrong about what the full remedy is. A synthetic GOROOT is not merely expensive — measured, it is incorrect. See §3.


1. The class, as measured

go test runs a package’s tests with the working directory set to that package’s source directory inside GOROOT. The converted host runs them in an isolated sandbox. The sandbox already reproduced the ancestry’s shapeTestHost.PackageDirectoryPath mirrors the whole import path, so the working directory’s own name and its parents are named as Go names them — but the parents were empty. Every member of the class reads something real above itself and therefore failed on layout, not on behavior:

Package What it reads above the package directory
io/ioutil lists .. and expects the sibling io package’s own io_test.go
internal/godebugs ../../../doc/godebug.md, plus go list -f= std cmd
go/parser ../printer/nodes.go, in a package-level var initializer
internal/testenv stats ../../../bin/go and compares it to exec.LookPath("go") with os.SameFile
internal/coverage/cfile a go.mod above testdata, and a location the toolchain accepts for internal imports
go/build ImportDir(cwd) must resolve cwd to the import path go/build

Two of these were recorded on the board from verdict names rather than sources and are corrected here:


2. The remedy — an ancestry, deliberately not a GOROOT

PackageAncestry (src/core/testing/PackageAncestry.cs) stages, inside the run sandbox, GOROOT’s content from its top level down to the package’s own directory:

GOROOT itself is not repointed. The host continues to report the real Go installation.

Environment fidelity, not just layout

Two further gaps surfaced while walking the class, both fixed at the same layer for the same reason — reproducing go test’s execution environment is the harness’s job:


3. Why NOT a synthetic GOROOT — the measurement that decided it

The board expected the remedy to be a synthetic GOROOT the host is pointed at. It cannot be, and the reason is specific rather than economic: a linked mirror is not walk-equivalent to the real tree.

Go reports a junction from Lstat as an irregular file, so filepath.WalkDir steps over it instead of descending. Measured on a junction-mirrored root against Go 1.23.1:

Probe Real GOROOT Mirrored root
WalkDir(root) counting *.gz 4 0
WalkDir(src/unicode) entries 19 1
Lstat(src/unicode) mode drwxrwxrwx ?rw-rw-rw-

Reads through a junction are faithful; only walks are not. Two already-validated packages walk GOROOT that way — compress/gzip’s issue14937 test (expected to find some .gz files under GOROOT) and path/filepath (walks filepath.Join(GOROOT, "src", "unicode")) — so repointing GOROOT at a synthetic view would have regressed them. Leaving GOROOT real costs nothing for this class, because every member resolves against its working directory.

This is also why the class is two roots, not one:

  1. CWD-ancestry — five members, closed by this view.
  2. GOROOT-identitygo/build alone. ImportDir(cwd) derives the import path by relating cwd to the GOROOT the process reports, so it is satisfiable only by repointing GOROOT (regresses two banked packages, above) or by running in the real GOROOT (lets tests write into the Go installation). It stays censused at 57/58, and this is now a measured wall rather than an inherited one.

4. Safety

Junctions inside a sandbox that is later deleted are a real hazard, and it is handled explicitly.


5. Stack headroom (found while walking the class, fixed here)

With its initializer wall gone, go/parser ran and died with an uncatchable Stack overflow. in parseType -> parseFieldDecl -> parseStructType -> tryIdentOrType, taking every later verdict with it. This is not the ancestry class; it is the host’s stack reservation.

Go’s parser guards recursion with maxNestLev = 1e5 and TestParseDepthLimit feeds it maxNestLev+1 deliberately, so Go recurses 100,001 levels — about four converted frames each, ~400k frames — before its own guard fires. Go serves that from goroutine stacks that grow to ~1 GB. The host’s dedicated per-test thread reserved 256 MB, which suffices only if every frame fits in 671 bytes; converted frames do not. TestThreadStackSize is raised to Go’s own 1 GB ceiling. The reservation costs address space only — pages commit on demand — which is why the existing design already chose a large reservation; it was simply sized at a fraction of Go’s rather than at Go’s.