Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions go/internal/e2e/agent_and_compact_rpc_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import (
)

func TestAgentSelectionRPCE2E(t *testing.T) {
cliPath := testharness.CLIPath()
if cliPath == "" {
t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.")
}
cliPath := testharness.CLIPath(t)

t.Run("should list available custom agents", func(t *testing.T) {
client := copilot.NewClient(&copilot.ClientOptions{
Expand Down
5 changes: 1 addition & 4 deletions go/internal/e2e/client_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import (
)

func TestClientE2E(t *testing.T) {
cliPath := testharness.CLIPath()
if cliPath == "" {
t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.")
}
cliPath := testharness.CLIPath(t)

t.Run("should start and connect to server using stdio", func(t *testing.T) {
client := copilot.NewClient(&copilot.ClientOptions{
Expand Down
5 changes: 1 addition & 4 deletions go/internal/e2e/inprocess_ffi_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ func TestInProcessFfiE2E(t *testing.T) {
t.Skip("in-process FFI smoke test runs only under the inprocess transport cell")
}

cliPath := testharness.CLIPath()
if cliPath == "" {
t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.")
}
cliPath := testharness.CLIPath(t)
t.Setenv("COPILOT_CLI_PATH", cliPath)

t.Run("should start and connect over in-process FFI", func(t *testing.T) {
Expand Down
5 changes: 1 addition & 4 deletions go/internal/e2e/rpc_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import (
)

func TestRPCE2E(t *testing.T) {
cliPath := testharness.CLIPath()
if cliPath == "" {
t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.")
}
cliPath := testharness.CLIPath(t)

t.Run("should call RPC.Ping with typed params and result", func(t *testing.T) {
client := copilot.NewClient(&copilot.ClientOptions{
Expand Down
91 changes: 81 additions & 10 deletions go/internal/e2e/testharness/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

copilot "github.com/github/copilot-sdk/go"
"github.com/github/copilot-sdk/go/internal/ffihost"
)

const defaultGitHubToken = "fake-token-for-e2e-tests"
Expand All @@ -21,7 +22,9 @@ var (
)

// CLIPath returns the path to the Copilot CLI, discovering it once and caching.
func CLIPath() string {
func CLIPath(t testing.TB) string {
t.Helper()

cliPathOnce.Do(func() {
// Check environment variable first
if path := os.Getenv("COPILOT_CLI_PATH"); path != "" {
Expand All @@ -34,15 +37,86 @@ func CLIPath() string {
// index.js ships in the installed platform package
// (e.g. @github/copilot-linux-x64).
base := RepoPath("nodejs", "node_modules", "@github")
matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js"))
if len(matches) > 0 {
cliPath = matches[0]
return
}
packageNames := cliPlatformPackageNames(ffihost.PrebuildsFolder())
cliPath = findCLIInNodeModules(base, packageNames)
})

if fileExists(cliPath) {
return cliPath
}

if envPath := os.Getenv("COPILOT_CLI_PATH"); envPath != "" {
t.Fatalf("CLI not found at %q from COPILOT_CLI_PATH", envPath)
}

base := RepoPath("nodejs", "node_modules", "@github")
packageNames := cliPlatformPackageNames(ffihost.PrebuildsFolder())
installed := installedCLIPackageNames(base)
triedDescription := strings.Join(packageNames, ", ")
if triedDescription == "" {
triedDescription = "none (unsupported platform)"
}
installedDescription := strings.Join(installed, ", ")
if installedDescription == "" {
installedDescription = "none"
}
t.Fatalf(
"CLI not found for tests under %s (tried: %s; present: %s). Run 'npm install' in the nodejs directory, or set COPILOT_CLI_PATH.",
base,
triedDescription,
installedDescription,
)
return cliPath
}

func cliPlatformPackageNames(platform string) []string {
if platform == "" {
return nil
}

names := []string{"copilot-" + platform}
if !strings.HasPrefix(platform, "linux") {
return names
}

_, arch, ok := strings.Cut(platform, "-")
if !ok {
return names
}
for _, variant := range []string{"linux-" + arch, "linuxmusl-" + arch} {
name := "copilot-" + variant
if name != names[0] {
names = append(names, name)
}
}
return names
}

func findCLIInNodeModules(githubModules string, packageNames []string) string {
for _, packageName := range packageNames {
candidate := filepath.Join(githubModules, packageName, "index.js")
if fileExists(candidate) {
return candidate
}
}
return ""
}

func installedCLIPackageNames(githubModules string) []string {
entries, err := os.ReadDir(githubModules)
if err != nil {
return nil
}

var names []string
for _, entry := range entries {
if entry.IsDir() && strings.HasPrefix(entry.Name(), "copilot-") {
names = append(names, entry.Name())
}
}
return names
}

// TestContext holds shared resources for E2E tests.
type TestContext struct {
CLIPath string
Expand Down Expand Up @@ -118,10 +192,7 @@ func SkipIfInProcess(t *testing.T, reason string) {
func NewTestContext(t *testing.T) *TestContext {
t.Helper()

cliPath := CLIPath()
if cliPath == "" || !fileExists(cliPath) {
t.Fatalf("CLI not found at %s. Run 'npm install' in the nodejs directory first.", cliPath)
}
cliPath := CLIPath(t)

homeDir, err := os.MkdirTemp("", "copilot-test-config-")
if err != nil {
Expand Down
112 changes: 112 additions & 0 deletions go/internal/e2e/testharness/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package testharness

import (
"os"
"path/filepath"
"reflect"
"testing"
)

func TestCLIPlatformPackageNames(t *testing.T) {
tests := []struct {
name string
platform string
want []string
}{
{
name: "macOS",
platform: "darwin-arm64",
want: []string{"copilot-darwin-arm64"},
},
{
name: "Windows",
platform: "win32-x64",
want: []string{"copilot-win32-x64"},
},
{
name: "Linux glibc",
platform: "linux-x64",
want: []string{"copilot-linux-x64", "copilot-linuxmusl-x64"},
},
{
name: "Linux musl",
platform: "linuxmusl-arm64",
want: []string{"copilot-linuxmusl-arm64", "copilot-linux-arm64"},
},
{
name: "unsupported platform",
platform: "",
want: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cliPlatformPackageNames(tt.platform); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("cliPlatformPackageNames(%q) = %v, want %v", tt.platform, got, tt.want)
}
})
}
}

func TestFindCLIInNodeModulesSelectsCurrentPlatform(t *testing.T) {
githubModules := t.TempDir()
makeCLIEntrypoint(t, githubModules, "copilot-darwin-arm64")
makeCLIEntrypoint(t, githubModules, "copilot-language-server")
want := makeCLIEntrypoint(t, githubModules, "copilot-linux-x64")

got := findCLIInNodeModules(githubModules, []string{"copilot-linux-x64", "copilot-linuxmusl-x64"})
if got != want {
t.Fatalf("findCLIInNodeModules() = %q, want %q", got, want)
}
}

func TestFindCLIInNodeModulesUsesCandidateOrder(t *testing.T) {
githubModules := t.TempDir()
makeCLIEntrypoint(t, githubModules, "copilot-linux-x64")
want := makeCLIEntrypoint(t, githubModules, "copilot-linuxmusl-x64")

got := findCLIInNodeModules(githubModules, []string{"copilot-linuxmusl-x64", "copilot-linux-x64"})
if got != want {
t.Fatalf("findCLIInNodeModules() = %q, want %q", got, want)
}
}

func TestFindCLIInNodeModulesReturnsEmptyWithoutCandidate(t *testing.T) {
githubModules := t.TempDir()
makeCLIEntrypoint(t, githubModules, "copilot-win32-x64")
if err := os.Mkdir(filepath.Join(githubModules, "copilot-linux-x64"), 0o755); err != nil {
t.Fatalf("Mkdir(): %v", err)
}

if got := findCLIInNodeModules(githubModules, []string{"copilot-linux-x64"}); got != "" {
t.Fatalf("findCLIInNodeModules() = %q, want empty path", got)
}
}

func TestInstalledCLIPackageNames(t *testing.T) {
githubModules := t.TempDir()
makeCLIEntrypoint(t, githubModules, "copilot-win32-x64")
makeCLIEntrypoint(t, githubModules, "copilot-darwin-arm64")
if err := os.Mkdir(filepath.Join(githubModules, "not-copilot"), 0o755); err != nil {
t.Fatalf("Mkdir(): %v", err)
}

want := []string{"copilot-darwin-arm64", "copilot-win32-x64"}
if got := installedCLIPackageNames(githubModules); !reflect.DeepEqual(got, want) {
t.Fatalf("installedCLIPackageNames() = %v, want %v", got, want)
}
}

func makeCLIEntrypoint(t *testing.T, githubModules, packageName string) string {
t.Helper()
packageDir := filepath.Join(githubModules, packageName)
if err := os.MkdirAll(packageDir, 0o755); err != nil {
t.Fatalf("MkdirAll(): %v", err)
}
entrypoint := filepath.Join(packageDir, "index.js")
if err := os.WriteFile(entrypoint, []byte("// test CLI entrypoint\n"), 0o600); err != nil {
t.Fatalf("WriteFile(): %v", err)
}
return entrypoint
}
42 changes: 40 additions & 2 deletions go/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,52 @@ fi
if [ -z "$COPILOT_CLI_PATH" ]; then
# Try to find it relative to the SDK. As of CLI 1.0.64-1 the @github/copilot
# package is a thin loader; the runnable index.js ships in the installed
# platform package (e.g. @github/copilot-linux-x64). Exactly one is installed.
# platform package (e.g. @github/copilot-linux-x64).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
POTENTIAL_PATH="$(ls "$SCRIPT_DIR"/../nodejs/node_modules/@github/copilot-*/index.js 2>/dev/null | head -n1)"
case "$(go env GOHOSTARCH)" in
amd64) NPM_ARCH="x64" ;;
arm64) NPM_ARCH="arm64" ;;
*)
echo "❌ Unsupported Go architecture: $(go env GOHOSTARCH)"
exit 1
;;
esac

NPM_PLATFORMS=()
case "$(go env GOHOSTOS)" in
darwin) NPM_PLATFORMS=("darwin") ;;
windows) NPM_PLATFORMS=("win32") ;;
linux)
if ldd --version 2>&1 | grep -qi musl; then
NPM_PLATFORMS=("linuxmusl" "linux")
else
NPM_PLATFORMS=("linux" "linuxmusl")
fi
;;
*)
echo "❌ Unsupported Go platform: $(go env GOHOSTOS)"
exit 1
;;
esac

POTENTIAL_PATH=""
TRIED_PACKAGES=()
for NPM_PLATFORM in "${NPM_PLATFORMS[@]}"; do
PACKAGE_NAME="copilot-${NPM_PLATFORM}-${NPM_ARCH}"
TRIED_PACKAGES+=("@github/${PACKAGE_NAME}")
CANDIDATE="$SCRIPT_DIR/../nodejs/node_modules/@github/${PACKAGE_NAME}/index.js"
if [ -f "$CANDIDATE" ]; then
POTENTIAL_PATH="$CANDIDATE"
break
fi
done

if [ -n "$POTENTIAL_PATH" ] && [ -f "$POTENTIAL_PATH" ]; then
export COPILOT_CLI_PATH="$POTENTIAL_PATH"
echo "📍 Auto-detected CLI path: $COPILOT_CLI_PATH"
else
echo "❌ COPILOT_CLI_PATH environment variable not set"
echo " Tried platform packages: ${TRIED_PACKAGES[*]}"
echo " Run: export COPILOT_CLI_PATH=/path/to/dist-cli/index.js"
exit 1
fi
Expand Down