// The cases below are the `edit` CLI's own tests (tools/edit/internal/*), // ported: what they guard is that the built-in tools refuse exactly what the // CLI refuses, in words a model can act on — otherwise the A/B comparison of // this part would measure two different tools. package fileedit import ( "os" "os/exec" "path/filepath" "strings" "testing" ) func inDir(t *testing.T, name, content string) string { t.Helper() path := filepath.Join(t.TempDir(), name) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } return path } func fileOf(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return string(data) } func TestApply(t *testing.T) { cases := []struct { name string body string edits []Edit want string }{ {"one replacement", "const port = 3000;\n", []Edit{{"const port = 3000;", "const port = envPort(3000);"}}, "const port = envPort(3000);\n"}, {"two independent edits, resolved against the original", "HOST = \"localhost\"\nPORT = 3000\n", []Edit{{`HOST = "localhost"`, `HOST = env("HOST", "localhost")`}, {`PORT = 3000`, `PORT = envInt("PORT", 3000)`}}, "HOST = env(\"HOST\", \"localhost\")\nPORT = envInt(\"PORT\", 3000)\n"}, {"the order of the edits does not change the result", "a\nb\nc\n", []Edit{{"c", "C"}, {"a", "A"}}, "A\nb\nC\n"}, {"an empty new deletes", "keep\nto delete\nkeep too\n", []Edit{{"to delete\n", ""}}, "keep\nkeep too\n"}, {"the old text spans several lines", "func f() {\n\treturn 1\n}\n", []Edit{{"func f() {\n\treturn 1\n}", "func f() int {\n\treturn 2\n}"}}, "func f() int {\n\treturn 2\n}\n"}, {"an edit is NOT re-read by the next one", "x\ny\n", []Edit{{"x", "y"}, {"y", "z"}}, "y\nz\n"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := apply(tc.body, tc.edits) if err != nil { t.Fatalf("apply: %v", err) } if got != tc.want { t.Errorf("apply = %q, want %q", got, tc.want) } }) } } // TestApplyRefuses guards the rules that make an edit verifiable. Each message // is read by a MODEL, which must be able to fix the call without guessing: the // test also checks that the message carries the fix. func TestApplyRefuses(t *testing.T) { cases := []struct { name string body string edits []Edit says string }{ {"text not found", "a\n", []Edit{{"b", "c"}}, "not found"}, {"ambiguous text", "x = 1\nx = 1\n", []Edit{{"x = 1", "x = 2"}}, "appears 2 times — add the surrounding lines until it is unique"}, {"overlapping regions", "func hello() {\n\treturn \"hello\"\n}\n", []Edit{{"func hello() {\n\treturn \"hello\"\n}", "func hello() {\n\treturn \"bonjour\"\n}"}, {"return \"hello\"", "return \"salut\""}}, "overlap"}, {"empty old", "a\n", []Edit{{"", "b"}}, "empty old text"}, {"an edit that changes nothing", "a\n", []Edit{{"a", "a"}}, "changes nothing"}, {"no edit at all", "a\n", nil, "no edit given"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := apply(tc.body, tc.edits) if err == nil { t.Fatalf("apply succeeded, want an error — result %q", got) } if !strings.Contains(err.Error(), tc.says) { t.Errorf("error = %q, want it to contain %q", err, tc.says) } if got != "" { t.Errorf("apply returned %q along with the error: nothing must be written", got) } }) } } func TestRead(t *testing.T) { path := inDir(t, "f.txt", "one\ntwo\nthree\n") got, err := Read(path, 0, 0, false) if err != nil || got != "one\ntwo\nthree\n" { t.Errorf("Read = (%q, %v)", got, err) } got, err = Read(path, 2, 2, true) if err != nil || got != " 2 two\n" { t.Errorf("Read 2-2 numbered = (%q, %v)", got, err) } if _, err := Read(filepath.Join(filepath.Dir(path), "nope.txt"), 0, 0, false); err == nil || !strings.Contains(err.Error(), "does not exist") { t.Errorf("Read of a missing file = %v", err) } if _, err := Read(path, 99, 0, false); err == nil || !strings.Contains(err.Error(), "past the end") { t.Errorf("Read start 99 = %v", err) } } func TestWriteCreatesAndReports(t *testing.T) { path := filepath.Join(t.TempDir(), "notes.md") r, err := Write(path, "# Notes") if err != nil { t.Fatal(err) } if got := fileOf(t, path); got != "# Notes\n" { t.Errorf("file = %q, want %q — the final newline is missing", got, "# Notes\n") } if !r.Created || !strings.Contains(r.Headline, "created") { t.Errorf("Result = %+v, want it to announce the creation", r) } // Rewriting the same content must do nothing, and SAY so. r, err = Write(path, "# Notes") if err != nil || r.Changed || !strings.Contains(r.Headline, "unchanged") { t.Errorf("identical Write = (%+v, %v)", r, err) } } func TestReplace(t *testing.T) { path := inDir(t, "server.go", "port := 3000\nhost := \"local\"\n") r, err := Replace(path, []Edit{{"port := 3000", "port := envPort(3000)"}}, false) if err != nil { t.Fatal(err) } if got := fileOf(t, path); got != "port := envPort(3000)\nhost := \"local\"\n" { t.Errorf("file = %q", got) } if r.FirstChangedLine != 1 || !strings.Contains(r.Headline, "first change at line 1") || !strings.Contains(r.Diff, "+ port := envPort(3000)") { t.Errorf("Result = %+v", r) } } // TestReplaceIsAllOrNothing: the second edit is impossible, so the first must // not have happened. A half-edited file is the worst result — it looks like a // success. func TestReplaceIsAllOrNothing(t *testing.T) { const before = "a = 1\nb = 2\n" path := inDir(t, "f.txt", before) _, err := Replace(path, []Edit{{"a = 1", "a = 10"}, {"absent", "x"}}, false) if err == nil || !strings.Contains(err.Error(), "not found") { t.Errorf("err = %v", err) } if got := fileOf(t, path); got != before { t.Errorf("file = %q, want unchanged %q", got, before) } } func TestReplaceRefusesAMissingFile(t *testing.T) { _, err := Replace(filepath.Join(t.TempDir(), "nope.txt"), []Edit{{"a", "b"}}, false) if err == nil || !strings.Contains(err.Error(), "does not exist") { t.Errorf("err = %v", err) } } func TestDryRunWritesNothing(t *testing.T) { const before = "a = 1\n" path := inDir(t, "f.txt", before) r, err := Replace(path, []Edit{{"a = 1", "a = 2"}}, true) if err != nil || !r.DryRun || !strings.Contains(r.Headline, "dry run") || !strings.Contains(r.Diff, "+ a = 2") { t.Errorf("dry run = (%+v, %v)", r, err) } if got := fileOf(t, path); got != before { t.Errorf("file = %q, want unchanged", got) } } // TestShapeSurvives: BOM and CRLF are removed for the edit and put back on // write, else every line of a Windows file would show up as changed. func TestShapeSurvives(t *testing.T) { cases := []struct{ name, before, old, new, after string }{ {"CRLF kept", "a\r\nb\r\n", "b", "B", "a\r\nB\r\n"}, {"BOM kept", "\uFEFFa\n", "a", "A", "\uFEFFA\n"}, {"plain LF", "a\n", "a", "A", "A\n"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { path := inDir(t, "f.txt", tc.before) if _, err := Replace(path, []Edit{{tc.old, tc.new}}, false); err != nil { t.Fatal(err) } if got := fileOf(t, path); got != tc.after { t.Errorf("file = %q, want %q", got, tc.after) } }) } } // TestWriteKeepsTheMode: an executable script that gets edited must stay // executable. func TestWriteKeepsTheMode(t *testing.T) { path := inDir(t, "s.sh", "echo a\n") if err := os.Chmod(path, 0o755); err != nil { t.Fatal(err) } if _, err := Write(path, "echo b\n"); err != nil { t.Fatal(err) } info, _ := os.Stat(path) if info.Mode().Perm() != 0o755 { t.Errorf("mode = %v, want 755", info.Mode().Perm()) } } func TestWriteCreatesParentDirs(t *testing.T) { path := filepath.Join(t.TempDir(), "a", "b", "notes.md") if _, err := Write(path, "# Notes\n"); err != nil { t.Fatalf("Write: %v", err) } if got := fileOf(t, path); got != "# Notes\n" { t.Errorf("file = %q", got) } } func TestFirstChanged(t *testing.T) { cases := []struct { name string old, new string want int }{ {"nothing changed", "a\nb\n", "a\nb\n", 0}, {"the second line", "a\nb\nc\n", "a\nB\nc\n", 2}, {"an addition at the top", "a\n", "zero\na\n", 1}, {"a deletion", "a\nb\nc\n", "a\nc\n", 2}, {"a created file", "", "a\n", 1}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := compare(tc.old, tc.new).firstChanged(); got != tc.want { t.Errorf("firstChanged = %d, want %d", got, tc.want) } }) } } func TestRenderHasNoANSI(t *testing.T) { out := compare("a\nb\n", "a\nB\n").render() if strings.Contains(out, "\033") || !strings.Contains(out, "- b") || !strings.Contains(out, "+ B") { t.Errorf("render = %q", out) } } // TestUnifiedMatchesGitApply is the test that counts: a patch that "looks // like" a unified patch is useless. git APPLIES it, and refuses anything that // is not exact — headers, line numbers, counts, end-of-file marker. func TestUnifiedMatchesGitApply(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git absent") } cases := []struct{ name, old, new string }{ {"one line in the middle", "a\nb\nc\nd\ne\n", "a\nb\nC\nd\ne\n"}, {"two distant areas", "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", "1\n2\nTHREE\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n"}, {"two neighbouring areas: one hunk", "1\n2\n3\n4\n5\n6\n7\n8\n", "1\n2\nTHREE\n4\nFIVE\n6\n7\n8\n"}, {"addition at the top", "a\nb\n", "zero\na\nb\n"}, {"deletion at the tail", "a\nb\nc\n", "a\nb\n"}, {"everything replaced", "a\nb\n", "x\ny\nz\n"}, {"no final newline", "a\nb\nc", "a\nB\nc"}, {"final newline added", "a\nb", "a\nb\n"}, {"file emptied", "a\nb\n", ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { patch := compare(tc.old, tc.new).unified("f.txt") if patch == "" { t.Fatal("empty patch") } dir := t.TempDir() run := func(args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %s: %v\n%s\npatch:\n%s", strings.Join(args, " "), err, out, patch) } } run("init", "-q") os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644) os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644) run("apply", "p.diff") if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new { t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch) } }) } }