parent_edit_integration_test.go

  1//go:build integration
  2
  3package main
  4
  5import (
  6	"os/exec"
  7	"strings"
  8	"testing"
  9
 10	"github.com/git-bug/git-bug/entities/bug"
 11	"github.com/git-bug/git-bug/entity"
 12	"github.com/git-bug/git-bug/repository"
 13)
 14
 15// setupParentEditRepo initializes a git repo with user and agent identities
 16func setupParentEditRepo(t *testing.T) string {
 17	t.Helper()
 18	tmpDir := t.TempDir()
 19
 20	initCmd := exec.Command("git", "init")
 21	initCmd.Dir = tmpDir
 22	if err := initCmd.Run(); err != nil {
 23		t.Fatalf("failed to init git repo: %v", err)
 24	}
 25
 26	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 27		t.Fatalf("failed to create user identity: %v", err)
 28	}
 29	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
 30		t.Fatalf("failed to create agent identity: %v", err)
 31	}
 32
 33	return tmpDir
 34}
 35
 36// findEditIssueByTitle returns the BugIssue with the given title
 37func findEditIssueByTitle(t *testing.T, repoPath, title string) BugIssue {
 38	t.Helper()
 39	issues, err := LoadBugs(repoPath)
 40	if err != nil {
 41		t.Fatalf("LoadBugs failed: %v", err)
 42	}
 43	for _, issue := range issues {
 44		if issue.Title == title {
 45			return issue
 46		}
 47	}
 48	t.Fatalf("bug with title %q not found", title)
 49	return BugIssue{}
 50}
 51
 52// createLinkedPair creates a parent bug and an unlinked child bug
 53func createLinkedPair(t *testing.T, repoPath string) (parent, child BugIssue) {
 54	t.Helper()
 55	if err := runNew(repoPath, "Edit Parent", "parent body", ""); err != nil {
 56		t.Fatalf("runNew parent failed: %v", err)
 57	}
 58	if err := runNew(repoPath, "Edit Child", "child body", ""); err != nil {
 59		t.Fatalf("runNew child failed: %v", err)
 60	}
 61	return findEditIssueByTitle(t, repoPath, "Edit Parent"),
 62		findEditIssueByTitle(t, repoPath, "Edit Child")
 63}
 64
 65// TestEditCommand_SetParent links an unlinked bug under a parent
 66func TestEditCommand_SetParent(t *testing.T) {
 67	tmpDir := setupParentEditRepo(t)
 68	parent, child := createLinkedPair(t, tmpDir)
 69
 70	if child.ParentID != "" {
 71		t.Fatalf("child already has parent %q", child.ParentID)
 72	}
 73
 74	if err := runEdit(tmpDir, child.ShortID, "", "", parent.ShortID); err != nil {
 75		t.Fatalf("runEdit set parent failed: %v", err)
 76	}
 77
 78	updated := findEditIssueByTitle(t, tmpDir, "Edit Child")
 79	if updated.ParentID != parent.FullID {
 80		t.Errorf("ParentID = %q, want parent full ID %q", updated.ParentID, parent.FullID)
 81	}
 82}
 83
 84// TestEditCommand_ReplaceParentError verifies parent links cannot be changed
 85func TestEditCommand_ReplaceParentError(t *testing.T) {
 86	tmpDir := setupParentEditRepo(t)
 87	parent, child := createLinkedPair(t, tmpDir)
 88
 89	if err := runNew(tmpDir, "Second Parent", "body", ""); err != nil {
 90		t.Fatalf("runNew second parent failed: %v", err)
 91	}
 92	second := findEditIssueByTitle(t, tmpDir, "Second Parent")
 93
 94	if err := runEdit(tmpDir, child.ShortID, "", "", parent.ShortID); err != nil {
 95		t.Fatalf("runEdit set parent failed: %v", err)
 96	}
 97
 98	err := runEdit(tmpDir, child.ShortID, "", "", second.ShortID)
 99	if err == nil {
100		t.Error("expected error when replacing an existing parent, got nil")
101	} else if !strings.Contains(err.Error(), "already has a parent") {
102		t.Errorf("expected 'already has a parent' error, got: %v", err)
103	}
104
105	// Original link must be intact
106	updated := findEditIssueByTitle(t, tmpDir, "Edit Child")
107	if updated.ParentID != parent.FullID {
108		t.Errorf("ParentID = %q, want original parent %q", updated.ParentID, parent.FullID)
109	}
110}
111
112// TestEditCommand_ParentCycleError verifies a parent link that would
113// close a dependency cycle is refused and the original state is kept
114func TestEditCommand_ParentCycleError(t *testing.T) {
115	tmpDir := setupParentEditRepo(t)
116	parent, child := createLinkedPair(t, tmpDir)
117
118	if err := runEdit(tmpDir, child.ShortID, "", "", parent.ShortID); err != nil {
119		t.Fatalf("runEdit set parent failed: %v", err)
120	}
121
122	err := runEdit(tmpDir, parent.ShortID, "", "", child.ShortID)
123	if err == nil {
124		t.Error("expected error for cyclic parent link, got nil")
125	} else if !strings.Contains(err.Error(), "cycle") {
126		t.Errorf("expected 'cycle' error, got: %v", err)
127	}
128
129	// Parent must remain link-free
130	updated := findEditIssueByTitle(t, tmpDir, "Edit Parent")
131	if updated.ParentID != "" {
132		t.Errorf("ParentID = %q, want link-free parent", updated.ParentID)
133	}
134}
135
136// TestEditCommand_InvalidParent tests error for unknown parent ID
137func TestEditCommand_InvalidParent(t *testing.T) {
138	tmpDir := setupParentEditRepo(t)
139	_, child := createLinkedPair(t, tmpDir)
140
141	err := runEdit(tmpDir, child.ShortID, "", "", "deadbee")
142	if err == nil {
143		t.Error("expected error for unknown parent ID, got nil")
144	} else if !strings.Contains(err.Error(), "deadbee") {
145		t.Errorf("expected error to mention parent ID, got: %v", err)
146	}
147}
148
149// TestEditCommand_SelfParent tests error when linking a bug to itself
150func TestEditCommand_SelfParent(t *testing.T) {
151	tmpDir := setupParentEditRepo(t)
152	_, child := createLinkedPair(t, tmpDir)
153
154	err := runEdit(tmpDir, child.ShortID, "", "", child.ShortID)
155	if err == nil {
156		t.Error("expected error for self parent, got nil")
157	} else if !strings.Contains(err.Error(), "own parent") {
158		t.Errorf("expected 'own parent' error, got: %v", err)
159	}
160}
161
162// TestEditCommand_ParentOnComment tests error when linking a comment
163func TestEditCommand_ParentOnComment(t *testing.T) {
164	tmpDir := setupParentEditRepo(t)
165	parent, _ := createLinkedPair(t, tmpDir)
166
167	if err := runComment(tmpDir, parent.FullID, "a comment"); err != nil {
168		t.Fatalf("runComment failed: %v", err)
169	}
170
171	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
172	if err != nil {
173		t.Fatalf("failed to open repo: %v", err)
174	}
175	b, err := bug.Read(repo, entity.Id(parent.FullID))
176	if err != nil {
177		t.Fatalf("failed to read bug: %v", err)
178	}
179	snap := b.Compile()
180	if len(snap.Comments) < 2 {
181		t.Fatal("expected at least 2 comments")
182	}
183	commentID := snap.Comments[1].CombinedId().String()
184	repo.Close()
185
186	err = runEdit(tmpDir, commentID, "", "", parent.ShortID)
187	if err == nil {
188		t.Error("expected error for parent flag on comment, got nil")
189	} else if !strings.Contains(err.Error(), "only applies to bugs") {
190		t.Errorf("expected 'only applies to bugs' error, got: %v", err)
191	}
192}
193
194// TestAgentEditCommand_SetParent links an unlinked bug as the agent
195func TestAgentEditCommand_SetParent(t *testing.T) {
196	tmpDir := setupParentEditRepo(t)
197	parent, child := createLinkedPair(t, tmpDir)
198
199	if err := runAgentEdit(tmpDir, child.ShortID, "", "", "", false, parent.ShortID); err != nil {
200		t.Fatalf("runAgentEdit set parent failed: %v", err)
201	}
202
203	updated := findEditIssueByTitle(t, tmpDir, "Edit Child")
204	if updated.ParentID != parent.FullID {
205		t.Errorf("ParentID = %q, want parent full ID %q", updated.ParentID, parent.FullID)
206	}
207}