diff --git a/internal/cli/run_engram_download_test.go b/internal/cli/run_engram_download_test.go index bc0ad5ea0..aebef85d8 100644 --- a/internal/cli/run_engram_download_test.go +++ b/internal/cli/run_engram_download_test.go @@ -3,6 +3,7 @@ package cli import ( "os" "path/filepath" + "runtime" "strings" "testing" @@ -502,6 +503,9 @@ func TestRunInstallBetaEngramUsesMainGoInstallAndInstalledBinary(t *testing.T) { home := t.TempDir() gobin := filepath.Join(home, "go-bin") betaEngram := filepath.Join(gobin, "engram") + if runtime.GOOS == "windows" { + betaEngram += ".exe" + } restoreCommand := runCommand restoreLookPath := cmdLookPath diff --git a/internal/components/engram/download_test.go b/internal/components/engram/download_test.go index 32fb08de0..3e94b60fc 100644 --- a/internal/components/engram/download_test.go +++ b/internal/components/engram/download_test.go @@ -1267,7 +1267,7 @@ func TestEngramGoInstallFromMain_UsesGoEnvForBinDir(t *testing.T) { t.Fatalf("engramGoInstallFromMain: unexpected error: %v", err) } - wantDir := fakeInstallDir + wantDir := filepath.FromSlash(fakeInstallDir) gotDir := filepath.Dir(binaryPath) if gotDir != wantDir { t.Errorf("binary dir = %q, want %q (from go env GOBIN)", gotDir, wantDir) @@ -1278,9 +1278,22 @@ func TestEngramGoInstallFromMain_BypassesPublicGoProxy(t *testing.T) { binDir := t.TempDir() goPath := filepath.Join(binDir, "go") recordPath := filepath.Join(t.TempDir(), "go-env.txt") - fakeGo := filepath.Join(binDir, "go") - script := "#!/usr/bin/env bash\n" + - "printf 'GONOSUMDB=%s\\nGOPRIVATE=%s\\nGONOPROXY=%s\\n' \"${GONOSUMDB:-}\" \"${GOPRIVATE:-}\" \"${GONOPROXY:-}\" > \"$GO_ENV_RECORD\"\n" + + var fakeGo string + var script string + if runtime.GOOS == "windows" { + fakeGo = filepath.Join(binDir, "go.bat") + script = "@echo off\n" + + "(\n" + + "echo GONOSUMDB=%GONOSUMDB%\n" + + "echo GOPRIVATE=%GOPRIVATE%\n" + + "echo GONOPROXY=%GONOPROXY%\n" + + ") > \"%GO_ENV_RECORD%\"\n" + } else { + fakeGo = filepath.Join(binDir, "go") + script = "#!/usr/bin/env bash\n" + + "printf 'GONOSUMDB=%s\\nGOPRIVATE=%s\\nGONOPROXY=%s\\n' \"${GONOSUMDB:-}\" \"${GOPRIVATE:-}\" \"${GONOPROXY:-}\" > \"$GO_ENV_RECORD\"\n" + } if err := os.WriteFile(fakeGo, []byte(script), 0o755); err != nil { t.Fatal(err) } diff --git a/internal/components/gga/config_test.go b/internal/components/gga/config_test.go index ec9021c5b..29c10daa6 100644 --- a/internal/components/gga/config_test.go +++ b/internal/components/gga/config_test.go @@ -3,6 +3,7 @@ package gga import ( "os" "path/filepath" + "runtime" "strings" "testing" @@ -119,6 +120,10 @@ func TestBuildConfigDifferentProviders(t *testing.T) { func TestInjectWritesConfigAndAgents(t *testing.T) { home := t.TempDir() + if runtime.GOOS == "windows" { + t.Setenv("APPDATA", filepath.Join(home, "AppData", "Roaming")) + } + result, err := Inject(home, []model.AgentID{model.AgentClaudeCode}) if err != nil { t.Fatalf("Inject() error = %v", err) @@ -163,6 +168,10 @@ func TestInjectWritesConfigAndAgents(t *testing.T) { func TestInjectIsIdempotent(t *testing.T) { home := t.TempDir() + if runtime.GOOS == "windows" { + t.Setenv("APPDATA", filepath.Join(home, "AppData", "Roaming")) + } + first, err := Inject(home, []model.AgentID{model.AgentOpenCode}) if err != nil { t.Fatalf("Inject() first error = %v", err) diff --git a/internal/components/permissions/inject_test.go b/internal/components/permissions/inject_test.go index df995b1ac..e7f298343 100644 --- a/internal/components/permissions/inject_test.go +++ b/internal/components/permissions/inject_test.go @@ -344,7 +344,6 @@ func TestInjectCodexWritesGentleDevPermissionsProfile(t *testing.T) { `"~/.gitconfig" = "read"`, `"~/.local/state/nix/profiles/home-manager/home-path" = "read"`, `"~/.nix-profile" = "read"`, - `"/nix/store" = "read"`, `":tmpdir" = "write"`, `":slash_tmp" = "write"`, `[permissions.gentle-dev.workspace_roots]`, @@ -365,6 +364,9 @@ func TestInjectCodexWritesGentleDevPermissionsProfile(t *testing.T) { `"**/*.key" = "deny"`, `"**/secrets/**" = "deny"`, } + if codexPermissionsGOOS != "windows" { + wantSubstrings = append(wantSubstrings, `"/nix/store" = "read"`) + } for _, want := range wantSubstrings { if !strings.Contains(text, want) { t.Fatalf("config.toml missing %q; got:\n%s", want, text) diff --git a/internal/components/uninstall/cleaners_test.go b/internal/components/uninstall/cleaners_test.go index 916e37eff..849f1ead0 100644 --- a/internal/components/uninstall/cleaners_test.go +++ b/internal/components/uninstall/cleaners_test.go @@ -243,7 +243,7 @@ func TestReadManagedFile_RejectsSymlink(t *testing.T) { } link := filepath.Join(dir, "link.json") if err := os.Symlink(target, link); err != nil { - t.Fatalf("Symlink() error = %v", err) + t.Skipf("skipping symlink test; symlink creation failed (likely due to missing privileges on Windows): %v", err) } _, err := readManagedFile(link) diff --git a/internal/tui/model_test.go b/internal/tui/model_test.go index 528c04187..b194bdb9e 100644 --- a/internal/tui/model_test.go +++ b/internal/tui/model_test.go @@ -897,7 +897,9 @@ func sddMultiCursor(t *testing.T) int { // opencode.json and otherwise shows its explicit empty state instead of silently // skipping model assignment. func TestSDDModeMultiShowsModelPickerWhenCacheMissing(t *testing.T) { - t.Setenv("HOME", t.TempDir()) + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) m := NewModel(system.DetectionResult{}, "dev") m.Screen = ScreenSDDMode @@ -917,7 +919,9 @@ func TestSDDModeMultiShowsModelPickerWhenCacheMissing(t *testing.T) { } func TestSDDModeMultiEmptyModelPickerCanContinueWithDefaults(t *testing.T) { - t.Setenv("HOME", t.TempDir()) + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) m := NewModel(system.DetectionResult{}, "dev") m.Screen = ScreenSDDMode @@ -6008,7 +6012,9 @@ func TestPickerFlowSlice(t *testing.T) { { name: "non-custom all agents SDDMode Multi cache absent excludes ModelPicker", setup: func(t *testing.T) Model { - t.Setenv("HOME", t.TempDir()) // guarantees cache path resolves to missing file + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) m := NewModel(system.DetectionResult{}, "dev") m.Selection.Preset = model.PresetFullGentleman m.Selection.Agents = allPickerAgents diff --git a/internal/update/check_test.go b/internal/update/check_test.go index f3770db37..181ff1e80 100644 --- a/internal/update/check_test.go +++ b/internal/update/check_test.go @@ -876,9 +876,9 @@ func TestCheckSingleTool_EngramUsesBinaryReleaseChannel(t *testing.T) { } execCommand = func(name string, args ...string) *exec.Cmd { if name == "engram" { - return exec.Command("echo", "engram 1.15.13") + return mockCmd("echo", "engram 1.15.13") } - return exec.Command("false") + return mockCmd("false") } result := checkSingleTool(context.Background(), Tools[1], "dev", system.PlatformProfile{OS: "darwin", PackageManager: "brew", Supported: true}) diff --git a/internal/update/install_script_test.go b/internal/update/install_script_test.go index 72a28095f..bc93fc8e0 100644 --- a/internal/update/install_script_test.go +++ b/internal/update/install_script_test.go @@ -70,7 +70,7 @@ func TestInstallScriptBetaGoInstallBypassesPublicGoProxy(t *testing.T) { t.Fatalf("ReadFile(%q) error = %v", path, err) } - script := string(content) + script := strings.ReplaceAll(string(content), "\r\n", "\n") for _, want := range []string{ "prepend_go_env_pattern GONOSUMDB github.com/gentleman-programming/gentle-ai", "prepend_go_env_pattern GOPRIVATE github.com/gentleman-programming/gentle-ai", @@ -103,7 +103,7 @@ func TestInstallScriptBetaGoInstallBypassesPublicGoProxy(t *testing.T) { } function := script[start : start+end+3] - cmd := exec.Command("bash", "-c", function+` + cmdStr := function + ` GONOSUMDB=example.com/private GOPRIVATE=github.com/acme/* GONOPROXY=github.com/gentleman-programming/gentle-ai @@ -111,13 +111,32 @@ prepend_go_env_pattern GONOSUMDB github.com/gentleman-programming/gentle-ai prepend_go_env_pattern GOPRIVATE github.com/gentleman-programming/gentle-ai prepend_go_env_pattern GONOPROXY github.com/gentleman-programming/gentle-ai printf '%s\n%s\n%s\n' "$GONOSUMDB" "$GOPRIVATE" "$GONOPROXY" -`) +` + cmdStr = strings.ReplaceAll(cmdStr, "\r", "") + tmpFile := "script_test_" + t.Name() + ".sh" + if err := os.WriteFile(tmpFile, []byte(cmdStr), 0o755); err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile) + cmd := exec.Command("bash", tmpFile) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("run prepend_go_env_pattern fixture: %v\noutput: %s", err, out) } - got := strings.TrimSpace(string(out)) + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + var cleanLines []string + for _, l := range lines { + l = strings.TrimSpace(l) + if strings.HasPrefix(l, "wsl:") { + continue + } + if l == "" { + continue + } + cleanLines = append(cleanLines, l) + } + got := strings.Join(cleanLines, "\n") want := strings.Join([]string{ "github.com/gentleman-programming/gentle-ai,example.com/private", "github.com/gentleman-programming/gentle-ai,github.com/acme/*", diff --git a/internal/update/upgrade/executor_test.go b/internal/update/upgrade/executor_test.go index 7d366dc37..cb455e569 100644 --- a/internal/update/upgrade/executor_test.go +++ b/internal/update/upgrade/executor_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/gentleman-programming/gentle-ai/internal/backup" + "github.com/gentleman-programming/gentle-ai/internal/components/gga" "github.com/gentleman-programming/gentle-ai/internal/model" "github.com/gentleman-programming/gentle-ai/internal/state" "github.com/gentleman-programming/gentle-ai/internal/system" @@ -856,8 +857,12 @@ func TestConfigPathsForBackup_CoversRegistryAgentsNotInOldList(t *testing.T) { func TestConfigPathsForBackup_GGAExtrasAreIncluded(t *testing.T) { homeDir := t.TempDir() - // Create GGA config file at ~/.config/gga/config - ggaConfigFile := filepath.Join(homeDir, ".config", "gga", "config") + if runtime.GOOS == "windows" { + t.Setenv("APPDATA", filepath.Join(homeDir, "AppData", "Roaming")) + } + + // Create GGA config file at the platform-appropriate path + ggaConfigFile := gga.ConfigPath(homeDir) if err := os.MkdirAll(filepath.Dir(ggaConfigFile), 0o755); err != nil { t.Fatalf("MkdirAll gga config: %v", err) } @@ -865,8 +870,8 @@ func TestConfigPathsForBackup_GGAExtrasAreIncluded(t *testing.T) { t.Fatalf("WriteFile gga config: %v", err) } - // Create GGA runtime lib file at ~/.local/share/gga/lib/pr_mode.sh - ggaLibFile := filepath.Join(homeDir, ".local", "share", "gga", "lib", "pr_mode.sh") + // Create GGA runtime lib file at the platform-appropriate path + ggaLibFile := gga.RuntimePRModePath(homeDir) if err := os.MkdirAll(filepath.Dir(ggaLibFile), 0o755); err != nil { t.Fatalf("MkdirAll gga lib: %v", err) } diff --git a/scripts/install.sh b/scripts/install.sh index 777b06eef..40588fec6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -311,7 +311,7 @@ install_go() { prepend_go_env_pattern() { local name="$1" local pattern="$2" - local current="${!name:-}" + eval "current=\${$name:-}" if [ -z "$current" ]; then printf -v "$name" '%s' "$pattern"