Files
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

387 lines
12 KiB
Go

package mcp
import (
"encoding/json"
"os"
"path/filepath"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func decodeFileOpsResult(t *testing.T, result *mcplib.CallToolResult) map[string]any {
t.Helper()
require.NotEmpty(t, result.Content)
text := result.Content[0].(mcplib.TextContent).Text
var resp map[string]any
require.NoError(t, json.Unmarshal([]byte(text), &resp))
return resp
}
func TestWriteFile_CreatesNewFile(t *testing.T) {
srv, dir := setupTestServer(t)
result := callTool(t, srv, "write_file", map[string]any{
"path": "docs/intro.md",
"content": "# Intro\n\nHello world.\n",
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "created", resp["status"])
got, err := os.ReadFile(filepath.Join(dir, "docs", "intro.md"))
require.NoError(t, err)
assert.Equal(t, "# Intro\n\nHello world.\n", string(got))
assert.Equal(t, float64(len(got)), resp["bytes_written"])
}
func TestWriteFile_OverwritesExistingFile(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "notes.txt")
require.NoError(t, os.WriteFile(target, []byte("old"), 0o644))
result := callTool(t, srv, "write_file", map[string]any{
"path": "notes.txt",
"content": "new content",
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "overwritten", resp["status"])
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "new content", string(got))
}
func TestWriteFile_AbsolutePath(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "absolute.txt")
result := callTool(t, srv, "write_file", map[string]any{
"path": target,
"content": "abs",
})
assert.False(t, result.IsError)
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "abs", string(got))
}
func TestWriteFile_ReindexesGoSource(t *testing.T) {
srv, _ := setupTestServer(t)
result := callTool(t, srv, "write_file", map[string]any{
"path": "main.go",
"content": "package main\n\nfunc freshlyAdded() {}\n",
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, true, resp["reindexed"], "Go file inside repo must trigger reindex")
search := callTool(t, srv, "search_symbols", map[string]any{"query": "freshlyAdded"})
searchResp := decodeFileOpsResult(t, search)
assert.Greater(t, searchResp["total"], float64(0), "new symbol should be searchable post-reindex")
}
func TestEditFile_UniqueReplacement(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "changelog.md")
require.NoError(t, os.WriteFile(target, []byte("v0.1 released\nv0.2 pending\n"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "changelog.md",
"old_string": "v0.2 pending",
"new_string": "v0.2 shipped",
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "applied", resp["status"])
assert.Equal(t, float64(1), resp["replacements"])
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "v0.1 released\nv0.2 shipped\n", string(got))
}
func TestEditFile_AmbiguousMatchRejected(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "dup.md")
require.NoError(t, os.WriteFile(target, []byte("TODO\nTODO\n"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "dup.md",
"old_string": "TODO",
"new_string": "DONE",
})
assert.True(t, result.IsError, "multiple matches without replace_all must error")
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "TODO\nTODO\n", string(got), "file must be untouched on error")
}
func TestEditFile_ReplaceAll(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "dup.md")
require.NoError(t, os.WriteFile(target, []byte("TODO\nTODO\nTODO\n"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "dup.md",
"old_string": "TODO",
"new_string": "DONE",
"replace_all": true,
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, float64(3), resp["replacements"])
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "DONE\nDONE\nDONE\n", string(got))
}
func TestEditFile_MissingOldString(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "file.md")
require.NoError(t, os.WriteFile(target, []byte("content"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "file.md",
"old_string": "not present",
"new_string": "x",
})
assert.True(t, result.IsError)
}
func TestEditFile_IdenticalStringsRejected(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "file.md")
require.NoError(t, os.WriteFile(target, []byte("content"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "file.md",
"old_string": "content",
"new_string": "content",
})
assert.True(t, result.IsError)
}
func TestEditFile_NonexistentFile(t *testing.T) {
srv, _ := setupTestServer(t)
result := callTool(t, srv, "edit_file", map[string]any{
"path": "does-not-exist.md",
"old_string": "a",
"new_string": "b",
})
assert.True(t, result.IsError)
}
func TestWriteFile_PreservesExistingMode(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "exec.sh")
require.NoError(t, os.WriteFile(target, []byte("old"), 0o755))
result := callTool(t, srv, "write_file", map[string]any{
"path": "exec.sh",
"content": "new",
})
assert.False(t, result.IsError)
info, err := os.Stat(target)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o755), info.Mode().Perm())
}
func TestWriteFile_CreatesNestedDirs(t *testing.T) {
srv, dir := setupTestServer(t)
result := callTool(t, srv, "write_file", map[string]any{
"path": "deeply/nested/sub/dir/notes.md",
"content": "hi",
})
assert.False(t, result.IsError, "atomic-write must mkdir -p parent dirs")
got, err := os.ReadFile(filepath.Join(dir, "deeply", "nested", "sub", "dir", "notes.md"))
require.NoError(t, err)
assert.Equal(t, "hi", string(got))
}
func TestWriteFile_RejectsPathTraversal(t *testing.T) {
srv, dir := setupTestServer(t)
// Marker file outside the repo root that the traversal would target.
parent := filepath.Dir(dir)
probe := filepath.Join(parent, "escape.txt")
t.Cleanup(func() { _ = os.Remove(probe) })
result := callTool(t, srv, "write_file", map[string]any{
"path": "../escape.txt",
"content": "should not land",
})
assert.True(t, result.IsError, "relative path with .. that escapes repo must be refused")
_, err := os.Stat(probe)
assert.True(t, os.IsNotExist(err), "no file may be written outside the repo root via traversal")
}
func TestEditFile_RejectsPathTraversal(t *testing.T) {
srv, dir := setupTestServer(t)
parent := filepath.Dir(dir)
probe := filepath.Join(parent, "outside.txt")
require.NoError(t, os.WriteFile(probe, []byte("untouched"), 0o644))
t.Cleanup(func() { _ = os.Remove(probe) })
result := callTool(t, srv, "edit_file", map[string]any{
"path": "../outside.txt",
"old_string": "untouched",
"new_string": "TAMPERED",
})
assert.True(t, result.IsError, "edit_file must refuse to follow traversal out of the repo")
got, err := os.ReadFile(probe)
require.NoError(t, err)
assert.Equal(t, "untouched", string(got), "the file outside the repo must remain unchanged")
}
func TestWriteFile_AbsolutePathOutsideRepoIsRefused(t *testing.T) {
// Confinement is universal (SECURITY.md): an absolute path that points
// outside every indexed repository root is refused, not honoured. This
// is the GHSA-w42c-h7hr-f67p arbitrary-write fix — previously such a
// path was written verbatim, an arbitrary-write -> RCE primitive.
srv, _ := setupTestServer(t)
tmp := t.TempDir()
target := filepath.Join(tmp, "external.txt")
result := callTool(t, srv, "write_file", map[string]any{
"path": target,
"content": "abs allowed",
})
assert.True(t, result.IsError, "an absolute path outside the repo must be refused")
_, statErr := os.Stat(target)
assert.True(t, os.IsNotExist(statErr), "the out-of-root file must not be created")
}
func TestEditFile_DryRunDoesNotWrite(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "preview.md")
require.NoError(t, os.WriteFile(target, []byte("original"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "preview.md",
"old_string": "original",
"new_string": "preview-only",
"dry_run": true,
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "would_apply", resp["status"])
assert.Equal(t, true, resp["dry_run"])
assert.Equal(t, float64(1), resp["replacements"])
assert.Equal(t, false, resp["reindexed"], "dry_run must NOT reindex")
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "original", string(got), "dry_run must NOT touch the file")
}
func TestWriteFile_DryRunReportsCreate(t *testing.T) {
srv, dir := setupTestServer(t)
result := callTool(t, srv, "write_file", map[string]any{
"path": "newfile.md",
"content": "preview",
"dry_run": true,
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "would_create", resp["status"])
assert.Equal(t, true, resp["dry_run"])
_, err := os.Stat(filepath.Join(dir, "newfile.md"))
assert.True(t, os.IsNotExist(err), "dry_run must NOT create the file")
}
func TestWriteFile_DryRunReportsOverwrite(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "existing.md")
require.NoError(t, os.WriteFile(target, []byte("v1"), 0o644))
result := callTool(t, srv, "write_file", map[string]any{
"path": "existing.md",
"content": "v2",
"dry_run": true,
})
assert.False(t, result.IsError)
resp := decodeFileOpsResult(t, result)
assert.Equal(t, "would_overwrite", resp["status"])
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "v1", string(got), "dry_run must NOT overwrite")
}
func TestWriteFile_RejectsDirectoryTarget(t *testing.T) {
srv, dir := setupTestServer(t)
require.NoError(t, os.MkdirAll(filepath.Join(dir, "is_a_dir"), 0o755))
result := callTool(t, srv, "write_file", map[string]any{
"path": "is_a_dir",
"content": "should fail",
})
assert.True(t, result.IsError, "write_file must refuse to clobber a directory")
}
func TestEditFile_AmbiguousErrorIncludesLineNumbers(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "many.md")
require.NoError(t, os.WriteFile(target, []byte("\nTODO\nTODO\nTODO\n"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "many.md",
"old_string": "TODO",
"new_string": "DONE",
})
assert.True(t, result.IsError)
require.NotEmpty(t, result.Content)
text := result.Content[0].(mcplib.TextContent).Text
assert.Contains(t, text, "first match lines", "error must surface the line numbers so the agent can choose a unique fragment")
}
func TestEditFile_DryRunAmbiguousFailsBeforeWrite(t *testing.T) {
srv, dir := setupTestServer(t)
target := filepath.Join(dir, "amb.md")
require.NoError(t, os.WriteFile(target, []byte("DUP\nDUP\n"), 0o644))
result := callTool(t, srv, "edit_file", map[string]any{
"path": "amb.md",
"old_string": "DUP",
"new_string": "x",
"dry_run": true,
})
assert.True(t, result.IsError, "dry_run must still surface ambiguity errors")
}
func TestPathContainedIn(t *testing.T) {
tests := []struct {
abs, root string
want bool
}{
{"/repo/a/b.go", "/repo", true},
{"/repo", "/repo", true},
{"/repo/", "/repo", true},
{"/repo-other/x.go", "/repo", false},
{"/etc/passwd", "/repo", false},
{"/repo/../etc/passwd", "/repo", false},
{"", "/repo", false},
{"/repo/x.go", "", false},
}
for _, tt := range tests {
got := pathContainedIn(tt.abs, tt.root)
assert.Equal(t, tt.want, got, "pathContainedIn(%q, %q)", tt.abs, tt.root)
}
}