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}