Repository navigation
[Go][tests] E2E harness picks the first @github/copilot-* package instead of the current platform package #2116
Description
Summary
The Go test harness resolves the Copilot CLI by globbing nodejs/node_modules/@github/copilot-*/index.js and taking matches[0] — the alphabetically first result — rather than the package matching the current OS/architecture. This logic lives in go/internal/e2e/testharness/context.go:36-41 and affects every E2E test that calls testharness.CLIPath().
This is the same defect #2103 reports for the Python harness, and the same one PR #2093 fixed for .NET.
// go/internal/e2e/testharness/context.go
base := RepoPath("nodejs", "node_modules", "@github")
matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js"))
if len(matches) > 0 {
cliPath = matches[0]
return
}Why This Is a Bug
filepath.Glob sorts its results, so matches[0] is whichever platform package sorts first — copilot-darwin-arm64 beats copilot-linux-x64 on a Linux host. The Node SDK resolves this correctly via getCliPlatformPackageNames() in nodejs/src/client.ts:357, which builds the candidate names from platform and architecture and tries both Linux libc variants (linux vs linuxmusl).
Two further problems in the same block:
- The glob is too loose.
copilot-*also matches non-platform packages under@github, such ascopilot-language-server. Any of those shipping anindex.jsbecomes a CLI candidate. - The no-match diagnostic is duplicated and uninformative.
CLIPath()returns""rather than an error, so every caller re-implements the same guard —NewTestContext(context.go:121-124), plusclient_e2e_test.go:12-15,rpc_e2e_test.go:13-16,agent_and_compact_rpc_e2e_test.go:15-18andinprocess_ffi_e2e_test.go:29-32, which each repeatif cliPath == "" { t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.") }. Nothing fails silently, but no message says which package was looked for, so "you never ran npm install" and "you have anode_modulesfor the wrong platform" — the situation this bug creates — are indistinguishable.NewTestContext's variant also interpolates the empty path, printing "CLI not found at .".
Where this actually bites
Worth noting for prioritisation: CI does not currently exercise this path. .github/workflows/go-sdk-tests.yml sets COPILOT_CLI_PATH from the setup-copilot action, and go/test.sh sets it too when unset, so CLIPath() returns from its env-var branch before reaching the glob. The bug shows up in local development — running go test ./internal/e2e/... directly from an editor or terminal without COPILOT_CLI_PATH.
Both of those env-var producers use the same loose pattern and have the same ordering defect:
go/test.sh:22—ls "$SCRIPT_DIR"/../nodejs/node_modules/@github/copilot-*/index.js | head -n1.github/actions/setup-copilot/action.yml:29—cli_path=$(ls "$(pwd)"/nodejs/node_modules/@github/copilot-*/index.js | head -n1)
go/test.sh is in scope here. The composite action is shared with the Java and Rust test workflows and the Java publish workflows, so it probably deserves its own change rather than riding along with a Go fix.
Repro Steps
- Leave
COPILOT_CLI_PATHunset - Have more than one
@github/copilot-*package present innodejs/node_modules/@github— a stale tree, anode_modulescache restored from a different runner, or a cross-platform container mount - Run the Go E2E tests directly:
cd go && go test ./internal/e2e/... CLIPath()returns the alphabetically first platform package- The test process launches an entrypoint built for another OS/arch
Expected Behavior
Build the candidate package names from the current platform and architecture, distinguishing the Linux glibc and musl variants, and probe those exact names — mirroring nodejs/src/client.ts.
Go already has this mapping. ffihost.PrebuildsFolder() in go/internal/ffihost/resolve.go:34-59 returns exactly the npm platform string the package directory is named after (linux-x64, linuxmusl-x64, darwin-arm64, win32-x64, …), including the musl probe, and returns "" on unsupported platforms. resolve.go is the only file in that package without the copilot_inprocess build tag and it imports nothing outside the standard library, so the harness can import it without pulling in the FFI machinery:
base := RepoPath("nodejs", "node_modules", "@github")
for _, name := range cliPlatformPackageNames() {
candidate := filepath.Join(base, name, "index.js")
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
cliPath = candidate
return
}
}
// no match: report the directory searched, the candidates tried, and COPILOT_CLI_PATHwhere cliPlatformPackageNames() returns "copilot-" + ffihost.PrebuildsFolder() plus, on Linux, the sibling libc variant — npm installs exactly one of them, and isMusl() can come up empty in minimal containers.
For the no-match case, giving CLIPath a testing.TB parameter so it can t.Fatalf with the directory searched and the candidates tried would let all five call sites drop their duplicated guard and would report the wrong-platform case distinctly. It's an internal package, so the signature change costs nothing outside the repo.
Related Issues
- #2103 — the same bug in the Python harness
- PR Fix active .NET and Python CI failures #2093 — the analogous .NET harness fix
- PR Update @github/copilot to 1.0.64-1 #1739 — introduced the platform-specific package layout in CLI 1.0.64-1
The Java harness (TestUtil.findCliPath) already builds copilot-<platform>-<cpuArch> explicitly and is unaffected.
Activity
github-actions commentedon Jul 28, 2026
Investigation Findings
Label applied:
bugI confirmed this is a genuine bug by reading the relevant source files.
What I found
go/internal/e2e/testharness/context.go(lines 36–41):filepath.Globreturns results in lexical order, somatches[0]is whichever platform package sorts alphabetically first —copilot-darwin-arm64would be selected on a Linux host if that package is present.Additional issues confirmed
copilot-*can match packages likecopilot-language-serverif they happen to have anindex.js.cliPathstays""andCLIPath()returns"".NewTestContextdoes callt.Fatalfif the path is empty, but the error message doesn't say which packages were tried — unlike the .NET and Python harnesses.Fix path
go/internal/ffihost/resolve.goalready hasPrebuildsFolder()which correctly returns the platform string (e.g.linux-x64,linuxmusl-x64,darwin-arm64,win32-x64). The harness can import this directly and buildcopilot-<platform>candidate names, probing both the glibc and musl variants on Linux, mirroring whatnodejs/src/client.tsdoes viagetCliPlatformPackageNames().This is the same class of bug as #2103 (Python harness), fixed for .NET in PR #2093.