123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 | package tools
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"blinker/internal/policy"
)
func TestWithTrailer(t *testing.T) {
tr := coauthor("gpt-x\n(evil) <a@b>")
if tr != "Co-authored-by: blinker (gpt-xevil a@b) <blinker@bruce-hill.com>" {
t.Errorf("coauthor = %q", tr)
}
for msg, want := range map[string]string{
"Fix it": "Fix it\n\n" + tr,
"Fix it\n\nBecause.": "Fix it\n\nBecause.\n\n" + tr,
"Fix it\n\nSigned-off-by: A <a@b>": "Fix it\n\nSigned-off-by: A <a@b>\n" + tr,
"Fix it\n\n" + tr: "Fix it\n\n" + tr,
"Fix: it": "Fix: it\n\n" + tr,
"Fix it\n\nNote: this, and\nmore.": "Fix it\n\nNote: this, and\nmore.\n\n" + tr,
} {
if got := withTrailer(msg, tr); got != want {
t.Errorf("withTrailer(%q) = %q, want %q", msg, got, want)
}
}
}
func TestParseBatch(t *testing.T) {
got, err := parseBatch("0000 missing\nabc blob 3\nhi\n\nabd blob 0\n\n", 3)
if err != nil || len(got) != 3 || got[0] != "" || got[1] != "hi\n" || got[2] != "" {
t.Errorf("parseBatch = %q, %v", got, err)
}
for _, bad := range []string{"abc blob 9\nhi\n", "abc tree 3\nhi\n\n", "abc blob 3\nhi\n\nextra", "abc blob 2\nhi\n\n"} {
if _, err := parseBatch(bad, 1); err == nil {
t.Errorf("parseBatch(%q) succeeded", bad)
}
}
}
func TestCommitReview(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
}
f := setup(t, nil, policy.ToolRule{Tool: "Commit", Access: policy.Ask})
f.tb.Model = "test-model"
git := func(args ...string) string {
t.Helper()
out, err := exec.Command("git", append([]string{"-C", f.project}, args...)...).CombinedOutput()
if err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
return string(out)
}
git("init", "-q")
git("config", "user.email", "t@example.com")
git("config", "user.name", "Test")
git("add", "main.go")
git("commit", "-qm", "First")
write := func(name, text string) {
t.Helper()
if err := os.WriteFile(filepath.Join(f.project, name), []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
write("main.go", "package main\n\nfunc main() { println(1) }\n")
write("new.go", "package main\n")
args := map[string]any{"message": "Print one", "files": []string{"main.go", "new.go"}}
// Denied with a note: nothing is committed, and the review showed the
// message with its trailer and the whole diff.
f.answer = func(q Question) Answer { return Answer{Option: q.Opts[3], Note: "say why"} }
r := f.run(t, "Commit", args)
if !r.Failed || !strings.Contains(r.Output, "say why") || len(f.asked) != 1 {
t.Fatalf("denied: %+v", r)
}
q := f.asked[0]
if !q.Pager || q.Body != "Print one\n\nCo-authored-by: blinker (test-model) <blinker@bruce-hill.com>" ||
!strings.Contains(q.Diff, "+func main() { println(1) }") || !strings.Contains(q.Diff, "+++ b/new.go") {
t.Errorf("review: %+v", q)
}
if len(q.Edits) != 3 || q.Edits[0].New != q.Body || q.Edits[1].Name != "main.go" || q.Edits[2].Old != "" {
t.Errorf("edits: %+v", q.Edits)
}
if git("rev-list", "--count", "HEAD") != "1\n" {
t.Error("a denied commit was made")
}
// Edited, then committed: the edits are reviewed again, and what is
// committed is what was shown. What else is staged stays staged.
write("other.go", "package main\n")
git("add", "other.go")
f.asked = nil
f.answer = func(q Question) Answer {
if len(f.asked) == 1 {
return Answer{Option: q.Opts[1], Edited: []string{"Print two\n", q.Edits[1].New, "package main // edited\n"}}
}
return Answer{Option: q.Opts[0]}
}
r = f.run(t, "Commit", args)
if r.Failed || len(f.asked) != 2 || !strings.HasPrefix(r.Output, "The user approved the commit, and it is done. Committed ") || !strings.Contains(r.Output, "the message") || !strings.Contains(r.Output, "+package main // edited") {
t.Fatalf("edited: %+v", r)
}
if f.asked[1].Body != "Print two" || !strings.Contains(f.asked[1].Diff, "+package main // edited") {
t.Errorf("second review: %+v", f.asked[1])
}
if got := git("log", "-1", "--format=%B"); got != "Print two\n\n" {
t.Errorf("message = %q", got)
}
if got := git("show", "HEAD:new.go"); got != "package main // edited\n" {
t.Errorf("new.go in the commit = %q", got)
}
if hash := strings.TrimSpace(git("rev-parse", "--short", "HEAD")); !strings.Contains(r.Summary, hash) || !strings.Contains(r.Detail, "Print two") {
t.Errorf("result: %+v", r)
}
if got := git("diff", "--cached", "--name-only"); got != "other.go\n" {
t.Errorf("staged after the commit: %q", got)
}
if got := git("status", "--porcelain", "--", "main.go", "new.go"); got != "" {
t.Errorf("status after the commit: %q", got)
}
// Nothing left to commit.
if r := f.run(t, "Commit", args); !r.Failed || !strings.Contains(r.Output, "nothing to commit") || !strings.Contains(r.Output, "Print two)") {
t.Errorf("empty commit: %+v", r)
}
}
// Commit's git sees files as the sandbox shows them: a hidden directory
// as empty. Committing that would delete the hidden files from the
// repository. And its paths are names, not patterns.
func TestCommitMatchesHost(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
}
f := setup(t, nil, policy.ToolRule{Tool: "Commit", Access: policy.Allow})
git := func(args ...string) string {
t.Helper()
out, err := exec.Command("git", append([]string{"-C", f.project}, args...)...).CombinedOutput()
if err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
return string(out)
}
prod := filepath.Join(f.project, "src", ".envs", "prod")
if err := os.MkdirAll(filepath.Dir(prod), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(prod, []byte("KEY=1\n"), 0o644); err != nil {
t.Fatal(err)
}
git("init", "-q")
git("config", "user.email", "t@example.com")
git("config", "user.name", "Test")
git("add", "-f", "src", "main.go")
git("commit", "-qm", "First")
if err := os.WriteFile(filepath.Join(f.project, "src", "a.go"), []byte("package src // changed\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(f.project, "main.go"), []byte("package main // changed\n"), 0o644); err != nil {
t.Fatal(err)
}
r := f.run(t, "Commit", map[string]any{"message": "Change src", "files": []string{"src"}})
if !r.Failed || !strings.Contains(r.Output, ".envs/prod") {
t.Errorf("committed over a hidden directory: %+v", r)
}
r = f.run(t, "Commit", map[string]any{"message": "Change Go files", "files": []string{"*.go"}})
if !r.Failed {
t.Errorf("a pattern was taken as paths: %+v", r)
}
if got := git("rev-list", "--count", "HEAD"); got != "1\n" {
t.Errorf("%s commits", strings.TrimSpace(got))
}
if got := git("ls-files", "src/.envs"); got != "src/.envs/prod\n" {
t.Errorf("tracked under src/.envs: %q", got)
}
}
// Output past the limit is reported as that, however the command dies of
// it.
func TestGitOutputLimit(t *testing.T) {
f := setup(t, nil)
_, err := f.tb.git(context.Background(), policy.SafeMode, nil, time.Minute, "", fmt.Sprintf("head -c %d /dev/zero", maxGitOutput+1))
if err == nil || !strings.Contains(err.Error(), "too much to review") {
t.Errorf("git = %v", err)
}
}
|