Repository navigation
Expand file tree
/
Copy pathadopt_test.go
More file actions
217 lines (198 loc) · 7.46 KB
/
Copy pathadopt_test.go
File metadata and controls
217 lines (198 loc) · 7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package specsync
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// TestAdoptWritesRefAndMarker covers the happy path: adopt writes
// .specsync/refs.json and adds the identity marker to the issue body.
func TestAdoptWritesRefAndMarker(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "changes", "my-change", "proposal.md"), "# My Change\n")
var editArgs []string
body := "the original body"
p := NewGitHubProviderFuncWithRepo("o/r", func(_ context.Context, args ...string) (string, error) {
switch {
case args[0] == "issue" && args[1] == "view":
return `{"number":42,"url":"https://github.com/o/r/issues/42","title":"T","body":"` + body + `","state":"OPEN"}`, nil
case args[0] == "issue" && args[1] == "edit":
editArgs = args
return "", nil
default:
return "", nil
}
})
res, err := Adopt(context.Background(), AdoptOptions{
OpenSpecDir: root,
Provider: p,
IssueID: "42",
Slug: "my-change",
})
if err != nil {
t.Fatalf("Adopt: %v", err)
}
if res.Slug != "my-change" || res.IssueID != "42" || res.IssueURL != "https://github.com/o/r/issues/42" {
t.Fatalf("unexpected result: %#v", res)
}
if !res.MarkerAdded {
t.Error("expected MarkerAdded true")
}
if editArgs == nil {
t.Fatal("expected an issue edit call adding the marker")
}
if got := flagValue(editArgs, "--body"); !strings.Contains(got, "specsync:change=my-change") || !strings.Contains(got, body) {
t.Errorf("edited body = %q, want marker + preserved content", got)
}
refs, err := LoadRefs(filepath.Join(root, "changes", "my-change"))
if err != nil {
t.Fatalf("LoadRefs: %v", err)
}
ref, ok := refs["github:o/r"]
if !ok || ref.ID != "42" {
t.Fatalf("refs = %#v, want github:o/r -> issue 42", refs)
}
}
// TestAdoptDryRunWritesNothing: a dry run must not touch the ref cache or
// the issue body.
func TestAdoptDryRunWritesNothing(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "changes", "my-change", "proposal.md"), "# My Change\n")
var wroteIssue bool
p := NewGitHubProviderFuncWithRepo("o/r", func(_ context.Context, args ...string) (string, error) {
switch {
case args[0] == "issue" && args[1] == "view":
return `{"number":42,"url":"https://github.com/o/r/issues/42","title":"T","body":"orig","state":"OPEN"}`, nil
case args[0] == "issue" && args[1] == "edit":
wroteIssue = true
return "", nil
default:
return "", nil
}
})
res, err := Adopt(context.Background(), AdoptOptions{
OpenSpecDir: root, Provider: p, IssueID: "42", Slug: "my-change", DryRun: true,
})
if err != nil {
t.Fatalf("Adopt: %v", err)
}
if !res.MarkerAdded {
t.Error("expected MarkerAdded true (preview) on dry run")
}
if wroteIssue {
t.Error("dry run must not edit the issue")
}
cdir := filepath.Join(root, "changes", "my-change")
if _, err := os.Stat(refCachePath(cdir)); !os.IsNotExist(err) {
t.Error("dry run must not write .specsync/refs.json")
}
}
// TestAdoptRejectsConflictingChangeBinding: the change already points at a
// different issue; -force is required to rebind.
func TestAdoptRejectsConflictingChangeBinding(t *testing.T) {
root := t.TempDir()
cdir := filepath.Join(root, "changes", "my-change")
mustWrite(t, filepath.Join(cdir, "proposal.md"), "# My Change\n")
if err := saveRef(cdir, "github:o/r", Ref{Provider: "github:o/r", ID: "10", URL: "https://github.com/o/r/issues/10"}); err != nil {
t.Fatalf("saveRef: %v", err)
}
p := NewGitHubProviderFuncWithRepo("o/r", func(_ context.Context, args ...string) (string, error) {
if args[0] == "issue" && args[1] == "view" {
return `{"number":42,"url":"https://github.com/o/r/issues/42","title":"T","body":"orig","state":"OPEN"}`, nil
}
return "", nil
})
if _, err := Adopt(context.Background(), AdoptOptions{
OpenSpecDir: root, Provider: p, IssueID: "42", Slug: "my-change",
}); err == nil {
t.Fatal("expected refusal to rebind an already-bound change without -force")
}
res, err := Adopt(context.Background(), AdoptOptions{
OpenSpecDir: root, Provider: p, IssueID: "42", Slug: "my-change", Force: true,
})
if err != nil {
t.Fatalf("Adopt with Force: %v", err)
}
if res.IssueID != "42" {
t.Errorf("IssueID = %q, want 42 after forced rebind", res.IssueID)
}
}
// TestAdoptRejectsConflictingIssueMarker: the issue already carries a marker
// for a different change; -force is required to steal it.
func TestAdoptRejectsConflictingIssueMarker(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "changes", "my-change", "proposal.md"), "# My Change\n")
p := NewGitHubProviderFuncWithRepo("o/r", func(_ context.Context, args ...string) (string, error) {
if args[0] == "issue" && args[1] == "view" {
return `{"number":42,"url":"https://github.com/o/r/issues/42","title":"T","body":"<!-- specsync:change=other-change -->\n\nbody","state":"OPEN"}`, nil
}
return "", nil
})
_, err := Adopt(context.Background(), AdoptOptions{
OpenSpecDir: root, Provider: p, IssueID: "42", Slug: "my-change",
})
if err == nil || !strings.Contains(err.Error(), "other-change") {
t.Fatalf("err = %v, want refusal naming the conflicting change", err)
}
}
// TestAdoptRegressionRace reproduces the observed FusionHub failure: a
// change is adopted onto an issue whose marker has not yet reached the
// tracker's search index. A later sync — which would previously find
// nothing via marker search and create a duplicate — must instead find the
// ref cache adopt wrote and update the same issue, never creating a second
// one, regardless of what marker search reports.
func TestAdoptRegressionRace(t *testing.T) {
root := t.TempDir()
cdir := filepath.Join(root, "changes", "my-change")
mustWrite(t, filepath.Join(cdir, "proposal.md"), "# My Change\n")
issueBody := "hand-created body"
var createCalled bool
viewResponse := func() string {
b, _ := json.Marshal(map[string]any{
"number": 42, "url": "https://github.com/o/r/issues/42",
"title": "T", "body": issueBody, "state": "OPEN", "labels": []string{},
})
return string(b)
}
run := func(_ context.Context, args ...string) (string, error) {
switch {
case args[0] == "issue" && args[1] == "view":
return viewResponse(), nil
case args[0] == "issue" && args[1] == "edit":
// Simulate the marker landing in the body for future Get calls,
// but the search index (below) stays stale for this run.
issueBody = "<!-- specsync:change=my-change -->\n\n" + issueBody
return "", nil
case args[0] == "issue" && args[1] == "list":
// The race: search hasn't indexed the marker yet.
return "[]", nil
case args[0] == "issue" && args[1] == "create":
createCalled = true
return "https://github.com/o/r/issues/999", nil
default:
return "", nil
}
}
p := NewGitHubProviderFuncWithRepo("o/r", run)
if _, err := Adopt(context.Background(), AdoptOptions{
OpenSpecDir: root, Provider: p, IssueID: "42", Slug: "my-change",
}); err != nil {
t.Fatalf("Adopt: %v", err)
}
// A sync run immediately after, while marker search is still lagging.
res, err := Sync(context.Background(), Options{OpenSpecDir: root, Provider: p})
if err != nil {
t.Fatalf("Sync: %v", err)
}
if createCalled {
t.Fatal("sync created a duplicate issue instead of using the ref adopt wrote")
}
if len(res.Items) != 1 || res.Items[0].URL != "https://github.com/o/r/issues/42" {
t.Fatalf("Sync result = %#v, want update of issue 42", res.Items)
}
if res.Created != 0 || res.Updated != 1 {
t.Fatalf("Created=%d Updated=%d, want 0/1 (update, not create)", res.Created, res.Updated)
}
}