From 58ee829acf5a758154cc39a387bf7d998f0167bc Mon Sep 17 00:00:00 2001 From: Andre Nogueira Date: Tue, 5 May 2026 11:28:37 +0100 Subject: [PATCH 1/2] fix(config,test): canonicalize NormalizeDir output; skip POSIX-only CLI tests on Windows NormalizeDir now converts backslashes to forward slashes after filepath.Abs, so stored Directories are platform-agnostic and match what we emit into the manifest. Without this, Windows produces mixed separators like `D:\tmp\foo/`. Five cmd-package tests assumed POSIX path semantics or HOME isolation: - TestDirAddAssignsAndRegenerates - TestDirAddRejectsDuplicate - TestDirRemoveUnassignsAndRegenerates - TestCurrentShowsEffectiveProfileInCwd - TestEndToEndDirectoryAssignment They hard-code `/tmp/...` paths (which absolutize differently on Windows) or rely on `t.Setenv("HOME", ...)` for isolation, which doesn't override USERPROFILE that os.UserHomeDir actually reads on Windows. Skip them there via a small skipOnWindows helper. Production behavior is still covered on Windows by the unit tests in internal/config and internal/git. Signed-off-by: Andre Nogueira --- cmd/cmd_test.go | 2 ++ cmd/dir_test.go | 22 ++++++++++++++++++++++ cmd/integration_test.go | 2 ++ internal/config/dirs.go | 5 +++++ 4 files changed, 31 insertions(+) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 5e5b4b9..82af633 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -605,6 +605,8 @@ func TestShowDisplaysDirectories(t *testing.T) { } func TestCurrentShowsEffectiveProfileInCwd(t *testing.T) { + skipOnWindows(t) + if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } diff --git a/cmd/dir_test.go b/cmd/dir_test.go index cda4320..3639c1c 100644 --- a/cmd/dir_test.go +++ b/cmd/dir_test.go @@ -3,13 +3,31 @@ package cmd import ( "os" "path/filepath" + "runtime" "strings" "testing" "github.com/aanogueira/git-context/internal/config" ) +// skipOnWindows bails out of CLI tests that hard-code POSIX-shaped paths +// (`/tmp/...`) or that rely on `t.Setenv("HOME", ...)` to isolate the +// home directory. Neither assumption holds on Windows: `/tmp/x` becomes +// `:\tmp\x` after `filepath.Abs`, and `os.UserHomeDir` reads +// `USERPROFILE` rather than `HOME`. The production code is exercised on +// Windows by the unit-level tests in `internal/config` and `internal/git`; +// this file's tests are CLI smoke checks rather than feature contracts. +func skipOnWindows(t *testing.T) { + t.Helper() + + if runtime.GOOS == "windows" { + t.Skip("uses POSIX path literals or HOME isolation; covered on Unix only") + } +} + func TestDirAddAssignsAndRegenerates(t *testing.T) { + skipOnWindows(t) + tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) @@ -43,6 +61,8 @@ func TestDirAddAssignsAndRegenerates(t *testing.T) { } func TestDirAddRejectsDuplicate(t *testing.T) { + skipOnWindows(t) + tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) @@ -91,6 +111,8 @@ func TestDirAddWarnsWhenNoDefaultProfile(t *testing.T) { } func TestDirRemoveUnassignsAndRegenerates(t *testing.T) { + skipOnWindows(t) + tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) diff --git a/cmd/integration_test.go b/cmd/integration_test.go index db7bb9d..a78dc0f 100644 --- a/cmd/integration_test.go +++ b/cmd/integration_test.go @@ -14,6 +14,8 @@ import ( // default profile, assign a directory to a different profile, verify that // inside the assigned directory git resolves the right user.email. func TestEndToEndDirectoryAssignment(t *testing.T) { + skipOnWindows(t) + if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } diff --git a/internal/config/dirs.go b/internal/config/dirs.go index 35bd4b2..28db7e9 100644 --- a/internal/config/dirs.go +++ b/internal/config/dirs.go @@ -17,6 +17,9 @@ import ( // git-style glob). // - `~` is expanded to the user's home directory. // - Relative paths are resolved against the current working directory. +// - On Windows, backslashes are normalized to forward slashes so the +// output is in the canonical form git config expects in `gitdir:` +// and `path =` values (and so storage in YAML is platform-agnostic). // - A trailing slash is always appended so the directive matches the // whole subtree, not just the directory itself. func NormalizeDir(path string) (string, error) { @@ -46,6 +49,8 @@ func NormalizeDir(path string) (string, error) { path = abs } + path = strings.ReplaceAll(path, `\`, `/`) + if !strings.HasSuffix(path, "/") { path += "/" } From 0d7ed1639b19d042d807879880b6f5e599247c36 Mon Sep 17 00:00:00 2001 From: Andre Nogueira Date: Tue, 5 May 2026 11:36:57 +0100 Subject: [PATCH 2/2] test: make NormalizeDir/WriteRootConfig/Regenerate tests Windows-portable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After NormalizeDir was canonicalized to forward slashes (and WriteRootConfig already runs all paths through toGitPath), the corresponding test expectations — built via filepath.Join, which uses OS-native separators — no longer matched on Windows. Run those expectations through the same slash-conversion as the production code. POSIX-rooted absolute paths like /Users/x/projects/work are not absolute on Windows (filepath.Abs resolves them against the cwd's drive), so the two NormalizeDir rows that use them are skipped on Windows. Equivalent semantics are still covered by the tilde and relative-path rows. Signed-off-by: Andre Nogueira --- internal/config/dirs_test.go | 38 ++++++++++++++++++++++++++---------- internal/git/git_test.go | 4 ++-- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/internal/config/dirs_test.go b/internal/config/dirs_test.go index 87d60c1..55169f8 100644 --- a/internal/config/dirs_test.go +++ b/internal/config/dirs_test.go @@ -3,10 +3,16 @@ package config import ( "os" "path/filepath" + "runtime" "strings" "testing" ) +// toFwd mirrors NormalizeDir's backslash-to-slash canonicalization so test +// expectations built via filepath.Join (which uses OS-native separators) +// stay comparable on Windows. +func toFwd(p string) string { return strings.ReplaceAll(p, `\`, `/`) } + func TestNormalizeDir(t *testing.T) { t.Parallel() @@ -20,23 +26,35 @@ func TestNormalizeDir(t *testing.T) { t.Fatalf("Getwd failed: %v", err) } - tests := []struct { + type tcase struct { name string in string want string - }{ - {"absolute path gets trailing slash", "/Users/x/projects/work", "/Users/x/projects/work/"}, - { - "absolute path keeps existing trailing slash", - "/Users/x/projects/work/", - "/Users/x/projects/work/", - }, - {"tilde expands to home", "~/projects/work", filepath.Join(home, "projects", "work") + "/"}, - {"relative resolves against cwd", "./foo", filepath.Join(cwd, "foo") + "/"}, + } + + tests := []tcase{ + {"tilde expands to home", "~/projects/work", toFwd(filepath.Join(home, "projects", "work")) + "/"}, + {"relative resolves against cwd", "./foo", toFwd(filepath.Join(cwd, "foo")) + "/"}, {"single-star glob passes through unchanged", "~/work/*/repo", "~/work/*/repo"}, {"double-star glob passes through unchanged", "~/work/**", "~/work/**"}, } + // POSIX-rooted absolute paths (`/Users/x/...`) are not absolute on + // Windows — `filepath.Abs` resolves them against the cwd's drive, + // producing `D:/Users/x/...`. Skip those rows on Windows; the + // equivalent semantics are exercised by the tilde + relative cases + // above, which build absolute paths via filepath.Join. + if runtime.GOOS != "windows" { + tests = append(tests, + tcase{"absolute path gets trailing slash", "/Users/x/projects/work", "/Users/x/projects/work/"}, + tcase{ + "absolute path keeps existing trailing slash", + "/Users/x/projects/work/", + "/Users/x/projects/work/", + }, + ) + } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 40b92f0..fbf1115 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -331,7 +331,7 @@ func TestWriteRootConfigDefaultAndAssignments(t *testing.T) { t.Errorf("missing [include] block:\n%s", content) } - if !strings.Contains(content, "path = "+defaultProfilePath) { + if !strings.Contains(content, "path = "+toGitPath(defaultProfilePath)) { t.Errorf("missing default profile path:\n%s", content) } @@ -464,7 +464,7 @@ func TestRegenerate(t *testing.T) { } rootStr := string(root) - wantInclude := filepath.Join(profilesDir, "work.gitconfig") + wantInclude := toGitPath(filepath.Join(profilesDir, "work.gitconfig")) if !strings.Contains(rootStr, "path = "+wantInclude) { t.Errorf("root missing default include for %q:\n%s", wantInclude, rootStr)