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
244 lines
7.7 KiB
Go
244 lines
7.7 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 decodeBatchEditResults(t *testing.T, res *mcplib.CallToolResult) []map[string]any {
|
|
t.Helper()
|
|
require.NotEmpty(t, res.Content)
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &resp))
|
|
raw, ok := resp["results"].([]any)
|
|
require.True(t, ok, "results array expected: %v", resp)
|
|
out := make([]map[string]any, len(raw))
|
|
for i, r := range raw {
|
|
out[i] = r.(map[string]any)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestBatchEdit_FileOpCRLF(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
target := filepath.Join(dir, "win.md")
|
|
require.NoError(t, os.WriteFile(target,
|
|
[]byte("alpha\r\nbeta\r\ngamma\r\n"), 0o644))
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_file",
|
|
"path": target,
|
|
"old_string": "beta\ngamma",
|
|
"new_string": "BETA\nGAMMA",
|
|
},
|
|
}})
|
|
require.False(t, res.IsError, "%v", res.Content)
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "applied", results[0]["status"])
|
|
assert.Equal(t, true, results[0]["eol_normalized"])
|
|
|
|
got, err := os.ReadFile(target)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "alpha\r\nBETA\r\nGAMMA\r\n", string(got))
|
|
assertUniformCRLF(t, string(got))
|
|
}
|
|
|
|
func TestBatchEdit_FileOpCRLFReplaceAll(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
target := filepath.Join(dir, "win2.md")
|
|
require.NoError(t, os.WriteFile(target,
|
|
[]byte("TODO x\r\nkeep\r\nTODO x\r\n"), 0o644))
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_file",
|
|
"path": target,
|
|
"old_string": "TODO x\n",
|
|
"new_string": "DONE x\n",
|
|
"replace_all": true,
|
|
},
|
|
}})
|
|
require.False(t, res.IsError, "%v", res.Content)
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "applied", results[0]["status"])
|
|
|
|
got, err := os.ReadFile(target)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "DONE x\r\nkeep\r\nDONE x\r\n", string(got))
|
|
assertUniformCRLF(t, string(got))
|
|
}
|
|
|
|
func TestBatchEdit_FileOpCRLFAmbiguousRejected(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
target := filepath.Join(dir, "win3.md")
|
|
original := "TODO x\r\nkeep\r\nTODO x\r\n"
|
|
require.NoError(t, os.WriteFile(target, []byte(original), 0o644))
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_file",
|
|
"path": target,
|
|
"old_string": "TODO x\n",
|
|
"new_string": "DONE x\n",
|
|
},
|
|
}})
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "failed", results[0]["status"])
|
|
assert.Contains(t, results[0]["error"], "matches 2 locations")
|
|
assert.Contains(t, results[0]["error"], "first match lines 1, 3")
|
|
|
|
got, err := os.ReadFile(target)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, original, string(got), "file must be untouched")
|
|
}
|
|
|
|
func TestBatchEdit_SymbolOpCRLF(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
path := writeCRLFGoFixture(t, srv, dir)
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_symbol",
|
|
"id": "crlf.go::CrlfTarget",
|
|
"old_source": "\ta := 1\n\tb := 2",
|
|
"new_source": "\ta := 10\n\tb := 20",
|
|
},
|
|
}})
|
|
require.False(t, res.IsError, "%v", res.Content)
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "applied", results[0]["status"])
|
|
assert.Equal(t, true, results[0]["eol_normalized"])
|
|
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(got), "\ta := 10\r\n\tb := 20\r\n",
|
|
"new_source must be written with CRLF endings")
|
|
assert.Contains(t, string(got), "func crlfSentinel() {\r\n\tc := 3\r\n",
|
|
"bytes after the edited symbol must be untouched — offset arithmetic")
|
|
assertUniformCRLF(t, string(got))
|
|
}
|
|
|
|
func TestBatchEdit_SymbolOpCRLFExactStillPreferred(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
path := writeCRLFGoFixture(t, srv, dir)
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_symbol",
|
|
"id": "crlf.go::CrlfTarget",
|
|
"old_source": "\ta := 1\r\n\tb := 2",
|
|
"new_source": "\ta := 10\r\n\tb := 20",
|
|
},
|
|
}})
|
|
require.False(t, res.IsError, "%v", res.Content)
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "applied", results[0]["status"])
|
|
assert.Nil(t, results[0]["eol_normalized"], "byte-exact match must not report eol_normalized")
|
|
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(got), "\ta := 10\r\n\tb := 20\r\n")
|
|
}
|
|
|
|
func TestBatchEdit_SymbolOpCRLFDocCommentIncludedInFragment(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
path := writeCRLFGoFixture(t, srv, dir)
|
|
|
|
// Parity with edit_symbol: the batch op must also expand the search
|
|
// window over the preceding doc comment, EOL-tolerantly.
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_symbol",
|
|
"id": "crlf.go::CrlfTarget",
|
|
"old_source": "// CrlfTarget exercises CRLF-file symbol editing.\nfunc CrlfTarget() {",
|
|
"new_source": "// CrlfTarget exercises CRLF-file symbol editing (edited).\nfunc CrlfTarget() {",
|
|
},
|
|
}})
|
|
require.False(t, res.IsError, "%v", res.Content)
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "applied", results[0]["status"], "error: %v", results[0]["error"])
|
|
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(got),
|
|
"// CrlfTarget exercises CRLF-file symbol editing (edited).\r\nfunc CrlfTarget() {\r\n")
|
|
assertUniformCRLF(t, string(got))
|
|
}
|
|
|
|
func TestBatchEdit_SymbolOpIdenticalSourcesRejected(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
path := writeCRLFGoFixture(t, srv, dir)
|
|
before, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_symbol",
|
|
"id": "crlf.go::CrlfTarget",
|
|
"old_source": "\ta := 1\r\n\tb := 2",
|
|
"new_source": "\ta := 1\r\n\tb := 2",
|
|
},
|
|
}})
|
|
results := decodeBatchEditResults(t, res)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "failed", results[0]["status"])
|
|
assert.Contains(t, results[0]["error"], "identical")
|
|
|
|
after, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(before), string(after), "no silent no-op write")
|
|
}
|
|
|
|
func TestBatchEdit_MixedOpsCRLF(t *testing.T) {
|
|
srv, dir := setupTestServer(t)
|
|
goPath := writeCRLFGoFixture(t, srv, dir)
|
|
mdPath := filepath.Join(dir, "notes.md")
|
|
require.NoError(t, os.WriteFile(mdPath,
|
|
[]byte("intro\r\nbody text\r\n"), 0o644))
|
|
|
|
res := callBatchEdit(t, srv, map[string]any{"edits": []any{
|
|
map[string]any{
|
|
"op": "edit_symbol",
|
|
"id": "crlf.go::CrlfTarget",
|
|
"old_source": "\t_ = a\n\t_ = b",
|
|
"new_source": "\t_, _ = a, b",
|
|
},
|
|
map[string]any{
|
|
"op": "edit_file",
|
|
"path": mdPath,
|
|
"old_string": "intro\nbody",
|
|
"new_string": "INTRO\nBODY",
|
|
},
|
|
}})
|
|
require.False(t, res.IsError, "%v", res.Content)
|
|
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &resp))
|
|
summary := resp["summary"].(map[string]any)
|
|
require.EqualValues(t, 2, summary["applied"])
|
|
require.EqualValues(t, 0, summary["failed"])
|
|
|
|
gotGo, err := os.ReadFile(goPath)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(gotGo), "\t_, _ = a, b\r\n")
|
|
assertUniformCRLF(t, string(gotGo))
|
|
|
|
gotMd, err := os.ReadFile(mdPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "INTRO\r\nBODY text\r\n", string(gotMd))
|
|
assertUniformCRLF(t, string(gotMd))
|
|
}
|