Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
370 lines
13 KiB
Go
370 lines
13 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormalizeGitPatchCwdForPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
windows bool
|
|
want string
|
|
}{
|
|
{
|
|
name: "windows git bash drive",
|
|
input: `/c/Users/demo/project`,
|
|
windows: true,
|
|
want: `C:/Users/demo/project`,
|
|
},
|
|
{
|
|
name: "windows git bash drive root",
|
|
input: `/d`,
|
|
windows: true,
|
|
want: `D:`,
|
|
},
|
|
{
|
|
name: "posix path remains posix",
|
|
input: `/c/Users/demo/project`,
|
|
windows: false,
|
|
want: `/c/Users/demo/project`,
|
|
},
|
|
{
|
|
name: "native windows path",
|
|
input: `C:\Users\demo\project`,
|
|
windows: true,
|
|
want: `C:/Users/demo/project`,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := normalizeGitPatchCwdForPlatform(tt.input, tt.windows); got != tt.want {
|
|
t.Fatalf("normalizeGitPatchCwdForPlatform(%q, %t) = %q, want %q", tt.input, tt.windows, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchRevertsDiffAndCleansTempDir(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "a.txt"), "old\n")
|
|
runGitForTest(t, dir, "add", "a.txt")
|
|
runGitForTest(t, dir, "commit", "-q", "-m", "init")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "a.txt"), "newer\n")
|
|
diff := gitOutputForPatchTest(t, dir, "diff", "--", "a.txt")
|
|
tempParent := t.TempDir()
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: dir,
|
|
Diff: diff,
|
|
Revert: true,
|
|
}, applyGitPatchOptions{TempParent: tempParent})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusSuccess {
|
|
t.Fatalf("status = %q, want %q, result=%#v", result.Status, ApplyGitPatchStatusSuccess, result)
|
|
}
|
|
if got := readTextForPatchTest(t, filepath.Join(dir, "a.txt")); got != "old\n" {
|
|
t.Fatalf("a.txt = %q, want old", got)
|
|
}
|
|
if !slices.Equal(result.AppliedPaths, []string{"a.txt"}) {
|
|
t.Fatalf("appliedPaths = %#v, want [a.txt]", result.AppliedPaths)
|
|
}
|
|
if entries := mustReadDirForPatchTest(t, tempParent); len(entries) != 0 {
|
|
t.Fatalf("temp parent still has entries after apply: %#v", entries)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchSupportsCwdRelativeDiffs(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
dir := t.TempDir()
|
|
subdir := filepath.Join(dir, "sub")
|
|
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
|
t.Fatalf("mkdir subdir: %v", err)
|
|
}
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
writeTextForPatchTest(t, filepath.Join(subdir, "a.txt"), "old\n")
|
|
runGitForTest(t, dir, "add", "sub/a.txt")
|
|
runGitForTest(t, dir, "commit", "-q", "-m", "init")
|
|
diff := "diff --git a/a.txt b/a.txt\n--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-old\n+new\n"
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: subdir,
|
|
Diff: diff,
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusSuccess {
|
|
t.Fatalf("status = %q, want %q, result=%#v", result.Status, ApplyGitPatchStatusSuccess, result)
|
|
}
|
|
if got := readTextForPatchTest(t, filepath.Join(subdir, "a.txt")); got != "new\n" {
|
|
t.Fatalf("sub/a.txt = %q, want new", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchRevertsCreatedUntrackedFileWithTrailingBlankLineDrift(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "README.md"), "base\n")
|
|
runGitForTest(t, dir, "add", "README.md")
|
|
runGitForTest(t, dir, "commit", "-q", "-m", "init")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "hello_world.md"), "# Hello, world!\n\n")
|
|
diff := "diff --git a/hello_world.md b/hello_world.md\nnew file mode 100644\n--- /dev/null\n+++ b/hello_world.md\n@@ -0,0 +1,1 @@\n+# Hello, world!\n"
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: dir,
|
|
Diff: diff,
|
|
Revert: true,
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusSuccess {
|
|
t.Fatalf("status = %q, want %q, result=%#v", result.Status, ApplyGitPatchStatusSuccess, result)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "hello_world.md")); !os.IsNotExist(err) {
|
|
t.Fatalf("hello_world.md still exists or stat failed with unexpected error: %v", err)
|
|
}
|
|
if !slices.Equal(result.AppliedPaths, []string{"hello_world.md"}) {
|
|
t.Fatalf("appliedPaths = %#v, want [hello_world.md]", result.AppliedPaths)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchDoesNotFallbackDeleteChangedCreatedFile(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "README.md"), "base\n")
|
|
runGitForTest(t, dir, "add", "README.md")
|
|
runGitForTest(t, dir, "commit", "-q", "-m", "init")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "hello_world.md"), "# Hello, world!\nchanged\n")
|
|
diff := "diff --git a/hello_world.md b/hello_world.md\nnew file mode 100644\n--- /dev/null\n+++ b/hello_world.md\n@@ -0,0 +1,1 @@\n+# Hello, world!\n"
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: dir,
|
|
Diff: diff,
|
|
Revert: true,
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status == ApplyGitPatchStatusSuccess {
|
|
t.Fatalf("status = %q, want non-success result=%#v", result.Status, result)
|
|
}
|
|
if got := readTextForPatchTest(t, filepath.Join(dir, "hello_world.md")); got != "# Hello, world!\nchanged\n" {
|
|
t.Fatalf("hello_world.md = %q, want changed file preserved", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchDoesNotPolluteRealIndex(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "a.txt"), "old\n")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "staged.txt"), "base\n")
|
|
runGitForTest(t, dir, "add", ".")
|
|
runGitForTest(t, dir, "commit", "-q", "-m", "init")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "staged.txt"), "staged\n")
|
|
runGitForTest(t, dir, "add", "staged.txt")
|
|
stagedBefore := gitOutputForPatchTest(t, dir, "diff", "--cached")
|
|
diff := "diff --git a/a.txt b/a.txt\n--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-old\n+new\n"
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: dir,
|
|
Diff: diff,
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusSuccess {
|
|
t.Fatalf("status = %q, want %q, result=%#v", result.Status, ApplyGitPatchStatusSuccess, result)
|
|
}
|
|
stagedAfter := gitOutputForPatchTest(t, dir, "diff", "--cached")
|
|
if stagedAfter != stagedBefore {
|
|
t.Fatalf("cached diff changed\nbefore:\n%s\nafter:\n%s", stagedBefore, stagedAfter)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchNonRepo(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: t.TempDir(),
|
|
Diff: "diff --git a/a.txt b/a.txt\n--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-old\n+new\n",
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusError || result.ErrorCode != ApplyGitPatchErrorNotGitRepo {
|
|
t.Fatalf("result = %#v, want not-git-repo error", result)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchRejectsCorruptPatchBeforeMutation(t *testing.T) {
|
|
dir := initGitPatchRepo(t)
|
|
filePath := filepath.Join(dir, "README.md")
|
|
if err := os.WriteFile(filePath, []byte("new"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: dir,
|
|
Diff: "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n \\ No newline at end of file\n+new\n \\ No newline at end of file\n",
|
|
Revert: true,
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusError || result.ErrorCode != ApplyGitPatchErrorInvalidPatch {
|
|
t.Fatalf("result = %#v, want invalid-patch error", result)
|
|
}
|
|
content, readErr := os.ReadFile(filePath)
|
|
if readErr != nil || string(content) != "new" {
|
|
t.Fatalf("README.md = %q, err=%v, want unchanged", content, readErr)
|
|
}
|
|
}
|
|
|
|
func TestApplyGitPatchReportsPatchDoesNotApply(t *testing.T) {
|
|
dir := initGitPatchRepo(t)
|
|
filePath := filepath.Join(dir, "README.md")
|
|
if err := os.WriteFile(filePath, []byte("different\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := applyGitPatchWithOptions(context.Background(), ApplyGitPatchInput{
|
|
Cwd: dir,
|
|
Diff: "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new\n",
|
|
}, applyGitPatchOptions{TempParent: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("applyGitPatchWithOptions returned error: %v", err)
|
|
}
|
|
if result.Status != ApplyGitPatchStatusError || result.ErrorCode != ApplyGitPatchErrorPatchDoesNotApply {
|
|
t.Fatalf("result = %#v, want patch-does-not-apply error", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveGitPatchSupportForPath(t *testing.T) {
|
|
t.Parallel()
|
|
requireGitForPatchTest(t)
|
|
|
|
service := &Service{}
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
|
|
supported, err := service.ResolveGitPatchSupportForPath(context.Background(), "workspace-1", dir)
|
|
if err != nil {
|
|
t.Fatalf("ResolveGitPatchSupportForPath returned error: %v", err)
|
|
}
|
|
if !supported.Supported || supported.Root == "" || supported.ErrorCode != ApplyGitPatchErrorNone {
|
|
t.Fatalf("supported = %#v, want supported git repo", supported)
|
|
}
|
|
|
|
filePath := filepath.Join(dir, "hello_world.md")
|
|
writeTextForPatchTest(t, filePath, "# Hello\n")
|
|
filePathSupported, err := service.ResolveGitPatchSupportForPath(context.Background(), "workspace-1", filePath)
|
|
if err != nil {
|
|
t.Fatalf("ResolveGitPatchSupportForPath file path returned error: %v", err)
|
|
}
|
|
if !filePathSupported.Supported || filePathSupported.Root != supported.Root || filePathSupported.ErrorCode != ApplyGitPatchErrorNone {
|
|
t.Fatalf("filePathSupported = %#v, want supported git repo rooted at %q", filePathSupported, supported.Root)
|
|
}
|
|
|
|
missingFileSupported, err := service.ResolveGitPatchSupportForPath(context.Background(), "workspace-1", filepath.Join(dir, "nested", "new.md"))
|
|
if err != nil {
|
|
t.Fatalf("ResolveGitPatchSupportForPath missing file path returned error: %v", err)
|
|
}
|
|
if !missingFileSupported.Supported || missingFileSupported.Root != supported.Root || missingFileSupported.ErrorCode != ApplyGitPatchErrorNone {
|
|
t.Fatalf("missingFileSupported = %#v, want supported git repo rooted at %q", missingFileSupported, supported.Root)
|
|
}
|
|
|
|
unsupported, err := service.ResolveGitPatchSupportForPath(context.Background(), "workspace-1", t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("ResolveGitPatchSupportForPath non-repo returned error: %v", err)
|
|
}
|
|
if unsupported.Supported || unsupported.ErrorCode != ApplyGitPatchErrorNotGitRepo {
|
|
t.Fatalf("unsupported = %#v, want not-git-repo", unsupported)
|
|
}
|
|
}
|
|
|
|
func requireGitForPatchTest(t *testing.T) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not available")
|
|
}
|
|
}
|
|
|
|
func initGitPatchRepo(t *testing.T) string {
|
|
t.Helper()
|
|
requireGitForPatchTest(t)
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init", "-q", "-b", "main")
|
|
writeTextForPatchTest(t, filepath.Join(dir, "README.md"), "old\n")
|
|
runGitForTest(t, dir, "add", "README.md")
|
|
runGitForTest(t, dir, "commit", "-q", "-m", "init")
|
|
return dir
|
|
}
|
|
|
|
func gitOutputForPatchTest(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(gitEnvScopedToDir(),
|
|
"GIT_CEILING_DIRECTORIES="+dir,
|
|
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t",
|
|
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
|
|
)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatalf("git %v: %v", args, err)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func writeTextForPatchTest(t *testing.T, path string, content string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func readTextForPatchTest(t *testing.T, path string) string {
|
|
t.Helper()
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
return string(content)
|
|
}
|
|
|
|
func mustReadDirForPatchTest(t *testing.T, path string) []os.DirEntry {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
t.Fatalf("read dir %s: %v", path, err)
|
|
}
|
|
return entries
|
|
}
|