Something went wrong. Try again.
An alternative CLI for git-bug that simplifies working with agents on issues
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340//go:build integration
package main
import ( "strings" "testing"
"github.com/git-bug/git-bug/entities/bug" "github.com/git-bug/git-bug/entities/common" "github.com/git-bug/git-bug/entity" "github.com/git-bug/git-bug/repository")
// readSnapshot reads a bug by full ID and compiles itfunc readSnapshot(t *testing.T, repoPath, fullID string) *bug.Snapshot { t.Helper() repo, err := repository.OpenGoGitRepo(repoPath, "", nil) if err != nil { t.Fatalf("failed to open repo: %v", err) } defer repo.Close() b, err := bug.Read(repo, entity.Id(fullID)) if err != nil { t.Fatalf("failed to read bug %s: %v", fullID, err) } return b.Compile()}
// findLiveIssueByTitle returns the bug with the given title that has not// been superseded by a clonefunc findLiveIssueByTitle(t *testing.T, repoPath, title string) BugIssue { t.Helper() issues, err := LoadBugs(repoPath) if err != nil { t.Fatalf("LoadBugs failed: %v", err) } var found []BugIssue for _, issue := range issues { if issue.Title == title && issue.SupersededBy == "" { found = append(found, issue) } } if len(found) != 1 { t.Fatalf("expected exactly one live bug titled %q, found %d", title, len(found)) } return found[0]}
// findIssueByID returns the bug with the given full IDfunc findIssueByID(t *testing.T, repoPath, fullID string) BugIssue { t.Helper() issues, err := LoadBugs(repoPath) if err != nil { t.Fatalf("LoadBugs failed: %v", err) } for _, issue := range issues { if issue.FullID == fullID { return issue } } t.Fatalf("bug %s not found", fullID) return BugIssue{}}
// newLinked creates a bug under parent (or root if parent is empty)func newLinked(t *testing.T, repoPath, title, parentID string) BugIssue { t.Helper() if err := runNew(repoPath, title, title+" body", parentID, "", ""); err != nil { t.Fatalf("runNew %q failed: %v", title, err) } return findLiveIssueByTitle(t, repoPath, title)}
// TestEditCommand_ChangeParentClonesBug verifies changing an existing parent// re-creates the bug under the new parent, preserving its history, and// closes the original as supersededfunc TestEditCommand_ChangeParentClonesBug(t *testing.T) { tmpDir := setupParentEditRepo(t) oldParent := newLinked(t, tmpDir, "Old Parent", "") newParent := newLinked(t, tmpDir, "New Parent", "") child := newLinked(t, tmpDir, "Child", oldParent.FullID)
if err := runComment(tmpDir, child.FullID, "user comment"); err != nil { t.Fatalf("runComment failed: %v", err) } if err := runAgentComment(tmpDir, child.FullID, "agent comment", "", false); err != nil { t.Fatalf("runAgentComment failed: %v", err) } if err := runEdit(tmpDir, child.FullID, "Child Renamed", "", "", ""); err != nil { t.Fatalf("runEdit title failed: %v", err) } original := readSnapshot(t, tmpDir, child.FullID)
if err := runEdit(tmpDir, child.FullID, "", "", newParent.FullID, ""); err != nil { t.Fatalf("runEdit change parent failed: %v", err) }
clone := findLiveIssueByTitle(t, tmpDir, "Child Renamed") if clone.FullID == child.FullID { t.Fatal("expected a new bug, got the original") } if clone.ParentID != newParent.FullID { t.Errorf("clone ParentID = %q, want %q", clone.ParentID, newParent.FullID) } if clone.Status != "open" { t.Errorf("clone status = %q, want open", clone.Status) }
cloned := readSnapshot(t, tmpDir, clone.FullID) if len(cloned.Comments) != len(original.Comments) { t.Fatalf("clone has %d comments, want %d", len(cloned.Comments), len(original.Comments)) } for i, want := range original.Comments { got := cloned.Comments[i] if got.Message != want.Message { t.Errorf("comment %d message = %q, want %q", i, got.Message, want.Message) } if got.Author.Id() != want.Author.Id() { t.Errorf("comment %d author = %s, want %s", i, got.Author.Name(), want.Author.Name()) } if got.FormatTime() != want.FormatTime() { t.Errorf("comment %d time = %s, want %s", i, got.FormatTime(), want.FormatTime()) } } if !cloned.CreateTime.Equal(original.CreateTime) { t.Errorf("clone CreateTime = %v, want %v", cloned.CreateTime, original.CreateTime) }
// Original is closed, still linked to its old parent, and points at the clone old := findIssueByID(t, tmpDir, child.FullID) if old.Status != "closed" { t.Errorf("original status = %q, want closed", old.Status) } if old.ParentID != oldParent.FullID { t.Errorf("original ParentID = %q, want %q", old.ParentID, oldParent.FullID) } if old.SupersededBy != clone.FullID { t.Errorf("original SupersededBy = %q, want %q", old.SupersededBy, clone.FullID) } oldSnap := readSnapshot(t, tmpDir, child.FullID) last := oldSnap.Comments[len(oldSnap.Comments)-1] if !strings.Contains(last.Message, clone.FullID[:7]) { t.Errorf("superseded comment %q should mention new ID %s", last.Message, clone.FullID[:7]) }}
// TestEditCommand_ChangeParentPreservesStatusAndLabels verifies closed// status and labels are replayed onto the clonefunc TestEditCommand_ChangeParentPreservesStatusAndLabels(t *testing.T) { tmpDir := setupParentEditRepo(t) oldParent := newLinked(t, tmpDir, "Old Parent", "") newParent := newLinked(t, tmpDir, "New Parent", "") child := newLinked(t, tmpDir, "Child", oldParent.FullID)
repo, err := repository.OpenGoGitRepo(tmpDir, "", nil) if err != nil { t.Fatalf("failed to open repo: %v", err) } b, err := bug.Read(repo, entity.Id(child.FullID)) if err != nil { t.Fatalf("failed to read bug: %v", err) } author := b.Compile().Author if _, _, err := bug.ChangeLabels(b, author, 1700000000, []string{"keep-me"}, nil, nil); err != nil { t.Fatalf("ChangeLabels failed: %v", err) } if err := b.Commit(repo); err != nil { t.Fatalf("commit failed: %v", err) } repo.Close() invalidateGitBugCache(tmpDir)
if err := runClose(tmpDir, child.FullID); err != nil { t.Fatalf("runClose failed: %v", err) }
if err := runEdit(tmpDir, child.FullID, "", "", newParent.FullID, ""); err != nil { t.Fatalf("runEdit change parent failed: %v", err) }
clone := findLiveIssueByTitle(t, tmpDir, "Child") if clone.Status != "closed" { t.Errorf("clone status = %q, want closed", clone.Status) } if len(clone.Labels) != 1 || clone.Labels[0] != "keep-me" { t.Errorf("clone labels = %v, want [keep-me]", clone.Labels) } if clone.ParentID != newParent.FullID { t.Errorf("clone ParentID = %q, want %q", clone.ParentID, newParent.FullID) } snap := readSnapshot(t, tmpDir, clone.FullID) if snap.Status != common.ClosedStatus { t.Errorf("clone snapshot status = %v, want closed", snap.Status) }}
// TestEditCommand_ChangeParentClonesDescendants verifies the whole subtree// is re-created so descendants point at the new IDsfunc TestEditCommand_ChangeParentClonesDescendants(t *testing.T) { tmpDir := setupParentEditRepo(t) oldParent := newLinked(t, tmpDir, "Old Parent", "") newParent := newLinked(t, tmpDir, "New Parent", "") child := newLinked(t, tmpDir, "Child", oldParent.FullID) grandchild := newLinked(t, tmpDir, "Grandchild", child.FullID)
if err := runEdit(tmpDir, child.FullID, "", "", newParent.FullID, ""); err != nil { t.Fatalf("runEdit change parent failed: %v", err) }
newChild := findLiveIssueByTitle(t, tmpDir, "Child") newGrandchild := findLiveIssueByTitle(t, tmpDir, "Grandchild") if newGrandchild.FullID == grandchild.FullID { t.Fatal("expected grandchild to be cloned") } if newGrandchild.ParentID != newChild.FullID { t.Errorf("grandchild ParentID = %q, want new child %q", newGrandchild.ParentID, newChild.FullID) }
oldGrandchild := findIssueByID(t, tmpDir, grandchild.FullID) if oldGrandchild.SupersededBy != newGrandchild.FullID { t.Errorf("old grandchild SupersededBy = %q, want %q", oldGrandchild.SupersededBy, newGrandchild.FullID) } if oldGrandchild.Status != "closed" { t.Errorf("old grandchild status = %q, want closed", oldGrandchild.Status) }}
// TestEditCommand_ChangeParentSkipsSupersededChildren verifies a child that// was already moved elsewhere is not cloned again with its former parentfunc TestEditCommand_ChangeParentSkipsSupersededChildren(t *testing.T) { tmpDir := setupParentEditRepo(t) a := newLinked(t, tmpDir, "A", "") b := newLinked(t, tmpDir, "B", "") c := newLinked(t, tmpDir, "C", "") child := newLinked(t, tmpDir, "Child", a.FullID) parentOfA := newLinked(t, tmpDir, "Parent Of A", c.FullID)
// Move Child from A to B: the original Child stays linked to A, superseded if err := runEdit(tmpDir, child.FullID, "", "", b.FullID, ""); err != nil { t.Fatalf("move child failed: %v", err) } // Link A under "Parent Of A", then move it under C if err := runEdit(tmpDir, a.FullID, "", "", parentOfA.FullID, ""); err != nil { t.Fatalf("link A failed: %v", err) } if err := runEdit(tmpDir, a.FullID, "", "", b.FullID, ""); err != nil { t.Fatalf("move A failed: %v", err) }
issues, err := LoadBugs(tmpDir) if err != nil { t.Fatalf("LoadBugs failed: %v", err) } count := 0 for _, issue := range issues { if issue.Title == "Child" { count++ } } if count != 2 { t.Errorf("found %d bugs titled Child, want 2 (original + one clone)", count) } liveChild := findLiveIssueByTitle(t, tmpDir, "Child") if liveChild.ParentID != b.FullID { t.Errorf("live child ParentID = %q, want B %q", liveChild.ParentID, b.FullID) }}
// TestEditCommand_ChangeParentOfSupersededBug verifies a superseded bug// cannot be moved and the user is pointed at its replacementfunc TestEditCommand_ChangeParentOfSupersededBug(t *testing.T) { tmpDir := setupParentEditRepo(t) oldParent := newLinked(t, tmpDir, "Old Parent", "") newParent := newLinked(t, tmpDir, "New Parent", "") child := newLinked(t, tmpDir, "Child", oldParent.FullID)
if err := runEdit(tmpDir, child.FullID, "", "", newParent.FullID, ""); err != nil { t.Fatalf("runEdit change parent failed: %v", err) } clone := findLiveIssueByTitle(t, tmpDir, "Child")
err := runEdit(tmpDir, child.FullID, "", "", oldParent.FullID, "") if err == nil { t.Fatal("expected error when moving a superseded bug, got nil") } if !strings.Contains(err.Error(), "superseded") || !strings.Contains(err.Error(), clone.FullID[:7]) { t.Errorf("expected error mentioning superseded and %s, got: %v", clone.FullID[:7], err) }}
// TestEditCommand_ChangeParentCycleError verifies moving a bug under its own// descendant is refusedfunc TestEditCommand_ChangeParentCycleError(t *testing.T) { tmpDir := setupParentEditRepo(t) root := newLinked(t, tmpDir, "Root", "") child := newLinked(t, tmpDir, "Child", root.FullID) grandchild := newLinked(t, tmpDir, "Grandchild", child.FullID)
err := runEdit(tmpDir, child.FullID, "", "", grandchild.FullID, "") if err == nil { t.Fatal("expected cycle error, got nil") } if !strings.Contains(err.Error(), "cycle") { t.Errorf("expected 'cycle' error, got: %v", err) } if got := findIssueByID(t, tmpDir, child.FullID); got.Status != "open" || got.SupersededBy != "" { t.Errorf("child should be untouched, got status %q superseded %q", got.Status, got.SupersededBy) }}
// TestAgentEditCommand_ChangeParentClonesBug verifies the agent API can// change an existing parentfunc TestAgentEditCommand_ChangeParentClonesBug(t *testing.T) { tmpDir := setupParentEditRepo(t) oldParent := newLinked(t, tmpDir, "Old Parent", "") newParent := newLinked(t, tmpDir, "New Parent", "") child := newLinked(t, tmpDir, "Child", oldParent.FullID)
if err := runAgentEdit(tmpDir, child.FullID, "", "", "", false, newParent.FullID, ""); err != nil { t.Fatalf("runAgentEdit change parent failed: %v", err) }
clone := findLiveIssueByTitle(t, tmpDir, "Child") if clone.ParentID != newParent.FullID { t.Errorf("clone ParentID = %q, want %q", clone.ParentID, newParent.FullID) } old := findIssueByID(t, tmpDir, child.FullID) if old.SupersededBy != clone.FullID || old.Status != "closed" { t.Errorf("original should be closed and superseded, got status %q superseded %q", old.Status, old.SupersededBy) }
// The superseded note is authored by the agent snap := readSnapshot(t, tmpDir, child.FullID) last := snap.Comments[len(snap.Comments)-1] if last.Author.Name() != "agent" { t.Errorf("superseded comment author = %q, want agent", last.Author.Name()) }}