Files
zzet--gortex/internal/mcp/tools_fileops_worktree_test.go
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

282 lines
11 KiB
Go

package mcp
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/indexer"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/parser/languages"
"github.com/zzet/gortex/internal/query"
"github.com/zzet/gortex/internal/search"
)
// gitInit runs git in dir, failing the test on error. Kept local so the
// fileops worktree test doesn't depend on a helper from another package.
func gitInit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(),
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_CONFIG_SYSTEM=/dev/null",
)
out, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git %v: %s", args, string(out))
}
// resolvePath evaluates symlinks so a macOS /var -> /private/var alias
// can't break a filesystem-path equality assertion.
func resolvePath(t *testing.T, p string) string {
t.Helper()
if r, err := filepath.EvalSymlinks(p); err == nil {
return r
}
return p
}
// setupWorktreePair builds a real git repo with one linked worktree.
// The main checkout and the worktree each hold the same repo-relative
// file (shared.go) plus their own file. Returns the main checkout path,
// the worktree path, and a Server whose MultiIndexer tracks both.
func setupWorktreePair(t *testing.T) (mainRepo, worktree string, srv *Server) {
t.Helper()
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git binary not available in PATH")
}
mainRepo = filepath.Join(t.TempDir(), "main-checkout")
require.NoError(t, os.MkdirAll(mainRepo, 0o755))
gitInit(t, mainRepo, "init", "-q", "-b", "main")
gitInit(t, mainRepo, "config", "user.email", "test@example.com")
gitInit(t, mainRepo, "config", "user.name", "Test")
gitInit(t, mainRepo, "config", "commit.gpgsign", "false")
require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "shared.go"),
[]byte("package main\n\nfunc Shared() string { return \"main\" }\n"), 0o644))
gitInit(t, mainRepo, "add", ".")
gitInit(t, mainRepo, "commit", "-q", "-m", "init")
// A linked worktree on its own branch. `git worktree add` checks
// out shared.go into it; the test then gives the worktree its own
// distinct copy so a misrooted write is unambiguous.
worktree = filepath.Join(t.TempDir(), "feature-worktree")
gitInit(t, mainRepo, "worktree", "add", "-q", "-b", "feature", worktree)
require.NoError(t, os.WriteFile(filepath.Join(worktree, "shared.go"),
[]byte("package main\n\nfunc Shared() string { return \"worktree\" }\n"), 0o644))
cfgPath := filepath.Join(t.TempDir(), "config.yaml")
gc := &config.GlobalConfig{Repos: []config.RepoEntry{
{Path: mainRepo, Name: "main-checkout"},
{Path: worktree, Name: "feature-worktree"},
}}
gc.SetConfigPath(cfgPath)
require.NoError(t, gc.Save())
cm, err := config.NewConfigManager(cfgPath)
require.NoError(t, err)
reg := parser.NewRegistry()
reg.Register(languages.NewGoExtractor())
g := graph.New()
mi := indexer.NewMultiIndexer(g, reg, search.NewBM25(), cm, zap.NewNop())
_, err = mi.IndexAll()
require.NoError(t, err)
require.True(t, mi.IsMultiRepo())
eng := query.NewEngine(g)
srv = NewServer(eng, g, nil, nil, zap.NewNop(), nil, MultiRepoOptions{
ConfigManager: cm,
MultiIndexer: mi,
})
return mainRepo, worktree, srv
}
// TestEditFile_LandsInLinkedWorktree is the core piece-2 scenario: an
// edit addressed to a file in a linked worktree must modify that
// worktree's copy on disk, not the main checkout's. Both checkouts
// share one repo identity for index-cache reuse, so the resolver has to
// re-root the write at the correct worktree.
func TestEditFile_LandsInLinkedWorktree(t *testing.T) {
mainRepo, worktree, srv := setupWorktreePair(t)
mainShared := filepath.Join(mainRepo, "shared.go")
wtShared := filepath.Join(worktree, "shared.go")
mainBefore, err := os.ReadFile(mainShared)
require.NoError(t, err)
// Edit shared.go via the worktree's repo prefix.
result := callTool(t, srv, "edit_file", map[string]any{
"path": "feature-worktree/shared.go",
"old_string": `return "worktree"`,
"new_string": `return "worktree-edited"`,
})
assert.False(t, result.IsError, "edit of a worktree file must succeed: %+v", result.Content)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "applied", resp["status"])
// The worktree's copy carries the edit.
wtAfter, err := os.ReadFile(wtShared)
require.NoError(t, err)
assert.Contains(t, string(wtAfter), `return "worktree-edited"`,
"the linked worktree's file must reflect the edit")
// The main checkout's copy is untouched — the edit did NOT leak
// into the main repo.
mainAfter, err := os.ReadFile(mainShared)
require.NoError(t, err)
assert.Equal(t, string(mainBefore), string(mainAfter),
"the main checkout's file must NOT be modified by a worktree-targeted edit")
}
// TestWriteFile_LandsInLinkedWorktree proves write_file (full-content
// overwrite) honours the same worktree rooting as edit_file.
func TestWriteFile_LandsInLinkedWorktree(t *testing.T) {
mainRepo, worktree, srv := setupWorktreePair(t)
mainShared := filepath.Join(mainRepo, "shared.go")
mainBefore, err := os.ReadFile(mainShared)
require.NoError(t, err)
result := callTool(t, srv, "write_file", map[string]any{
"path": "feature-worktree/shared.go",
"content": "package main\n\nfunc Shared() string { return \"rewritten-in-worktree\" }\n",
})
assert.False(t, result.IsError, "write to a worktree file must succeed: %+v", result.Content)
wtAfter, err := os.ReadFile(filepath.Join(worktree, "shared.go"))
require.NoError(t, err)
assert.Contains(t, string(wtAfter), "rewritten-in-worktree",
"write_file must land in the worktree's copy")
mainAfter, err := os.ReadFile(mainShared)
require.NoError(t, err)
assert.Equal(t, string(mainBefore), string(mainAfter),
"write_file targeting the worktree must not overwrite the main checkout")
}
// TestEditFile_MainCheckoutPrefixStillEditsMainCheckout is the other
// side of the rooting contract: an edit addressed via the *main*
// checkout's prefix must land in the main checkout, not bleed into a
// worktree. Worktree rooting must not hijack writes that legitimately
// target the main repo.
func TestEditFile_MainCheckoutPrefixStillEditsMainCheckout(t *testing.T) {
mainRepo, worktree, srv := setupWorktreePair(t)
wtShared := filepath.Join(worktree, "shared.go")
wtBefore, err := os.ReadFile(wtShared)
require.NoError(t, err)
result := callTool(t, srv, "edit_file", map[string]any{
"path": "main-checkout/shared.go",
"old_string": `return "main"`,
"new_string": `return "main-edited"`,
})
assert.False(t, result.IsError, "edit of a main-checkout file must succeed: %+v", result.Content)
mainAfter, err := os.ReadFile(filepath.Join(mainRepo, "shared.go"))
require.NoError(t, err)
assert.Contains(t, string(mainAfter), `return "main-edited"`,
"the main checkout's file must reflect the edit")
wtAfter, err := os.ReadFile(wtShared)
require.NoError(t, err)
assert.Equal(t, string(wtBefore), string(wtAfter),
"the linked worktree's file must NOT be modified by a main-checkout edit")
}
// fakeWorktreeLookup is a multiRepoLookup stub for testing
// worktreeRootedPath without standing up a full MultiIndexer.
type fakeWorktreeLookup struct {
worktrees map[string][]string // mainRepoPath -> linked worktree roots
}
func (f fakeWorktreeLookup) RepoPrefixes() []string { return nil }
func (f fakeWorktreeLookup) RepoRoot(string) (string, bool) { return "", false }
func (f fakeWorktreeLookup) LinkedWorktreeRoots(main string) []string { return f.worktrees[main] }
// TestWorktreeRootedPath covers the re-rooting helper's decision table
// directly: it re-roots only when a file genuinely lives in a sibling
// worktree, and leaves the path alone in every conservative case.
func TestWorktreeRootedPath(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git binary not available in PATH")
}
// Real git repo + worktree so ResolveWorktree classifies correctly.
mainRepo := filepath.Join(t.TempDir(), "main")
require.NoError(t, os.MkdirAll(mainRepo, 0o755))
gitInit(t, mainRepo, "init", "-q", "-b", "main")
gitInit(t, mainRepo, "config", "user.email", "test@example.com")
gitInit(t, mainRepo, "config", "user.name", "Test")
gitInit(t, mainRepo, "config", "commit.gpgsign", "false")
require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "seed.go"),
[]byte("package main\n"), 0o644))
gitInit(t, mainRepo, "add", ".")
gitInit(t, mainRepo, "commit", "-q", "-m", "init")
worktree := filepath.Join(t.TempDir(), "wt")
gitInit(t, mainRepo, "worktree", "add", "-q", "-b", "feature", worktree)
// A file that exists ONLY in the worktree.
require.NoError(t, os.WriteFile(filepath.Join(worktree, "only_wt.go"),
[]byte("package main\n"), 0o644))
// A file that exists in the main checkout.
require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "in_main.go"),
[]byte("package main\n"), 0o644))
// worktreeRootedPath passes the resolved root through to
// LinkedWorktreeRoots verbatim, so the stub is keyed by the same
// path string the helper will hand it.
mi := fakeWorktreeLookup{worktrees: map[string][]string{
mainRepo: {worktree},
worktree: {worktree},
}}
t.Run("re-roots a file that lives only in the worktree", func(t *testing.T) {
// Resolved under the main checkout, but the file is not there.
abs := filepath.Join(mainRepo, "only_wt.go")
got := worktreeRootedPath(abs, mainRepo, mi)
assert.Equal(t, resolvePath(t, filepath.Join(worktree, "only_wt.go")),
resolvePath(t, got),
"a file present only in the worktree must be re-rooted there")
})
t.Run("leaves a file that exists in the resolved root", func(t *testing.T) {
abs := filepath.Join(mainRepo, "in_main.go")
got := worktreeRootedPath(abs, mainRepo, mi)
assert.Equal(t, abs, got,
"a file that exists under the resolved root must not be moved")
})
t.Run("leaves a brand-new file under the named prefix", func(t *testing.T) {
// A file in no checkout — a fresh write_file. It must stay
// where the caller addressed it.
abs := filepath.Join(mainRepo, "brand_new.go")
got := worktreeRootedPath(abs, mainRepo, mi)
assert.Equal(t, abs, got,
"a new file must land under the prefix the caller named")
})
t.Run("leaves a path already inside a worktree", func(t *testing.T) {
// Root is the worktree itself — the file is already in the
// right checkout, nothing to re-root.
abs := filepath.Join(worktree, "only_wt.go")
got := worktreeRootedPath(abs, worktree, mi)
assert.Equal(t, abs, got,
"a path resolved against a worktree root must be left untouched")
})
t.Run("nil lookup is a no-op", func(t *testing.T) {
abs := filepath.Join(mainRepo, "only_wt.go")
assert.Equal(t, abs, worktreeRootedPath(abs, mainRepo, nil))
})
}