arche / internal/issues/issues_test.go

commit 154431fd
  1package issues_test
  2
  3import (
  4	"testing"
  5	"time"
  6
  7	"arche/internal/issues"
  8
  9	_ "github.com/mattn/go-sqlite3"
 10)
 11
 12func buildEvent(issueID, kind string, payload []byte, hlcMS int64, hlcSeq int) issues.IssueEvent {
 13	return issues.IssueEvent{
 14		EventID: kind + "-" + string(rune('a'+hlcSeq)),
 15		IssueID: issueID,
 16		HLCMS:   hlcMS,
 17		HLCSeq:  hlcSeq,
 18		Kind:    kind,
 19		Payload: payload,
 20		Author:  "test",
 21		Created: time.Now().Unix(),
 22	}
 23}
 24
 25func mustJSON(s string) []byte { return []byte(`"` + s + `"`) }
 26
 27func TestReduce_BasicIssue(t *testing.T) {
 28	evs := []issues.IssueEvent{
 29		buildEvent("id1", "create", []byte(`{"id":"id1"}`), 1, 0),
 30		buildEvent("id1", "title", mustJSON("hello"), 2, 0),
 31		buildEvent("id1", "status", mustJSON("open"), 3, 0),
 32		buildEvent("id1", "body", mustJSON("first body"), 4, 0),
 33	}
 34	iss := issues.Reduce(evs)
 35	if iss.Title != "hello" {
 36		t.Errorf("title: want %q got %q", "hello", iss.Title)
 37	}
 38	if iss.Status != "open" {
 39		t.Errorf("status: want open got %q", iss.Status)
 40	}
 41	if iss.Body != "first body" {
 42		t.Errorf("body: want %q got %q", "first body", iss.Body)
 43	}
 44}
 45
 46func TestReduce_StatusLWW(t *testing.T) {
 47	evs := []issues.IssueEvent{
 48		buildEvent("id1", "status", mustJSON("closed"), 10, 0),
 49		buildEvent("id1", "status", mustJSON("open"), 5, 0),
 50	}
 51	iss := issues.Reduce(evs)
 52	if iss.Status != "closed" {
 53		t.Errorf("LWW: want closed got %q", iss.Status)
 54	}
 55}
 56
 57func TestReduce_TitleLWW_SameMS_SeqBreaks(t *testing.T) {
 58	evs := []issues.IssueEvent{
 59		buildEvent("id1", "title", mustJSON("first"), 10, 0),
 60		buildEvent("id1", "title", mustJSON("second"), 10, 1),
 61	}
 62	iss := issues.Reduce(evs)
 63	if iss.Title != "second" {
 64		t.Errorf("seq tiebreak: want %q got %q", "second", iss.Title)
 65	}
 66}
 67
 68func TestReduce_Comments_AppendOrder(t *testing.T) {
 69	evs := []issues.IssueEvent{
 70		buildEvent("id1", "comment", mustJSON("first comment"), 1, 0),
 71		buildEvent("id1", "comment", mustJSON("second comment"), 2, 0),
 72		buildEvent("id1", "comment", mustJSON("third comment"), 3, 0),
 73	}
 74	iss := issues.Reduce(evs)
 75	if len(iss.Comments) != 3 {
 76		t.Fatalf("want 3 comments, got %d", len(iss.Comments))
 77	}
 78	if iss.Comments[0].Text != "first comment" {
 79		t.Errorf("comment[0]: want %q got %q", "first comment", iss.Comments[0].Text)
 80	}
 81	if iss.Comments[2].Text != "third comment" {
 82		t.Errorf("comment[2]: want %q got %q", "third comment", iss.Comments[2].Text)
 83	}
 84}
 85
 86func TestReduce_Labels_ORSet(t *testing.T) {
 87	evs := []issues.IssueEvent{
 88		buildEvent("id1", "label_add", []byte(`{"label":"bug","token":"tok1"}`), 1, 0),
 89		buildEvent("id1", "label_add", []byte(`{"label":"urgent","token":"tok2"}`), 2, 0),
 90		buildEvent("id1", "label_rm", mustJSON("tok1"), 3, 0),
 91	}
 92	iss := issues.Reduce(evs)
 93	if len(iss.Labels) != 1 {
 94		t.Fatalf("want 1 label, got %d: %v", len(iss.Labels), iss.Labels)
 95	}
 96	if iss.Labels[0] != "urgent" {
 97		t.Errorf("label: want urgent got %q", iss.Labels[0])
 98	}
 99}
100
101func TestReduce_Labels_AddTwiceRemoveOnce(t *testing.T) {
102	evs := []issues.IssueEvent{
103		buildEvent("id1", "label_add", []byte(`{"label":"bug","token":"tok1"}`), 1, 0),
104		buildEvent("id1", "label_add", []byte(`{"label":"bug","token":"tok2"}`), 2, 0),
105		buildEvent("id1", "label_rm", mustJSON("tok1"), 3, 0),
106	}
107	iss := issues.Reduce(evs)
108	if len(iss.Labels) != 1 || iss.Labels[0] != "bug" {
109		t.Errorf("OR-set: bug should survive remove of tok1; got %v", iss.Labels)
110	}
111}
112
113func TestReduce_Refs_GrowOnly(t *testing.T) {
114	evs := []issues.IssueEvent{
115		buildEvent("id1", "ref", mustJSON("abc123"), 1, 0),
116		buildEvent("id1", "ref", mustJSON("def456"), 2, 0),
117	}
118	iss := issues.Reduce(evs)
119	if len(iss.Refs) != 2 {
120		t.Fatalf("want 2 refs, got %d", len(iss.Refs))
121	}
122}
123
124func TestReduce_BodyConflict(t *testing.T) {
125	bc := `{"BaseEventID":"base","OurEdit":"ours","TheirEdit":"theirs"}`
126	evs := []issues.IssueEvent{
127		buildEvent("id1", "body_conflict", []byte(bc), 5, 0),
128	}
129	iss := issues.Reduce(evs)
130	if iss.BodyConflict == nil {
131		t.Fatal("expected BodyConflict, got nil")
132	}
133	if iss.BodyConflict.OurEdit != "ours" {
134		t.Errorf("OurEdit: want ours got %q", iss.BodyConflict.OurEdit)
135	}
136}
137
138func TestReduce_EmptyStatus_DefaultsOpen(t *testing.T) {
139	evs := []issues.IssueEvent{
140		buildEvent("id1", "create", []byte(`{"id":"id1"}`), 1, 0),
141	}
142	iss := issues.Reduce(evs)
143	if iss.Status != "open" {
144		t.Errorf("default status: want open got %q", iss.Status)
145	}
146}
147
148func openTestStore(t *testing.T) *issues.Store {
149	t.Helper()
150	s, err := issues.OpenForTesting()
151	if err != nil {
152		t.Fatalf("open store: %v", err)
153	}
154	t.Cleanup(func() { s.Close() })
155	return s
156}
157
158func TestStore_CreateAndGet(t *testing.T) {
159	s := openTestStore(t)
160
161	id, err := s.CreateIssue("fix the bug", "steps to reproduce", "alice")
162	if err != nil {
163		t.Fatalf("CreateIssue: %v", err)
164	}
165
166	iss, err := s.GetIssue(id)
167	if err != nil {
168		t.Fatalf("GetIssue: %v", err)
169	}
170	if iss.Title != "fix the bug" {
171		t.Errorf("title: want %q got %q", "fix the bug", iss.Title)
172	}
173	if iss.Status != "open" {
174		t.Errorf("status: want open got %q", iss.Status)
175	}
176	if iss.Body != "steps to reproduce" {
177		t.Errorf("body: want %q got %q", "steps to reproduce", iss.Body)
178	}
179}
180
181func TestStore_SetStatus(t *testing.T) {
182	s := openTestStore(t)
183	id, _ := s.CreateIssue("bug", "", "alice")
184
185	if err := s.SetStatus(id, "closed", "bob"); err != nil {
186		t.Fatalf("SetStatus: %v", err)
187	}
188	iss, _ := s.GetIssue(id)
189	if iss.Status != "closed" {
190		t.Errorf("want closed got %q", iss.Status)
191	}
192}
193
194func TestStore_AddComment(t *testing.T) {
195	s := openTestStore(t)
196	id, _ := s.CreateIssue("bug", "", "alice")
197
198	if err := s.AddComment(id, "this is a comment", "bob"); err != nil {
199		t.Fatalf("AddComment: %v", err)
200	}
201	iss, _ := s.GetIssue(id)
202	if len(iss.Comments) != 1 {
203		t.Fatalf("want 1 comment, got %d", len(iss.Comments))
204	}
205	if iss.Comments[0].Text != "this is a comment" {
206		t.Errorf("comment text: want %q got %q", "this is a comment", iss.Comments[0].Text)
207	}
208}
209
210func TestStore_ListIssues(t *testing.T) {
211	s := openTestStore(t)
212	s.CreateIssue("issue A", "", "alice") //nolint:errcheck
213	s.CreateIssue("issue B", "", "bob")   //nolint:errcheck
214
215	stubs, err := s.ListIssues()
216	if err != nil {
217		t.Fatalf("ListIssues: %v", err)
218	}
219	if len(stubs) != 2 {
220		t.Errorf("want 2 issues, got %d", len(stubs))
221	}
222}
223
224func TestStore_MergeEvents_Idempotent(t *testing.T) {
225	s := openTestStore(t)
226	id, _ := s.CreateIssue("bug", "body", "alice")
227
228	all, err := s.AllEvents()
229	if err != nil {
230		t.Fatalf("AllEvents: %v", err)
231	}
232
233	if err := s.MergeEvents(all); err != nil {
234		t.Fatalf("MergeEvents idempotent: %v", err)
235	}
236
237	iss, _ := s.GetIssue(id)
238	if iss.Title != "bug" {
239		t.Errorf("title after idempotent merge: want bug got %q", iss.Title)
240	}
241}
242
243func TestStore_MergeEvents_Union(t *testing.T) {
244	s1 := openTestStore(t)
245	s2 := openTestStore(t)
246
247	id, _ := s1.CreateIssue("shared bug", "", "alice")
248	s1.AddComment(id, "from s1", "alice") //nolint:errcheck
249
250	evs1, _ := s1.AllEvents()
251	s2.MergeEvents(evs1) //nolint:errcheck
252
253	s2.AddComment(id, "from s2", "bob") //nolint:errcheck
254
255	evs2, _ := s2.AllEvents()
256	s1.MergeEvents(evs2) //nolint:errcheck
257
258	iss1, _ := s1.GetIssue(id)
259	iss2, _ := s2.GetIssue(id)
260	if len(iss1.Comments) != 2 {
261		t.Errorf("s1 wants 2 comments, got %d", len(iss1.Comments))
262	}
263	if len(iss2.Comments) != 2 {
264		t.Errorf("s2 wants 2 comments, got %d", len(iss2.Comments))
265	}
266}