detached 9564f12

test(usecase): verify persisted merge resolution and reopening

commit
9564f1289fc77aeb7575974a3fcad343d8c1febc
author
brice · · 2026-10-22 18:36 UTC
committer
brice · · 2026-10-22 18:41 UTC
parent
ec131d8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +27 -32
M internal/usecase/issue_links_test.go +27 -32
@@ -5,7 +5,6 @@ package usecase_test
55 import (
66 "context"
77 "fmt"
8- "sync"
98 "sync/atomic"
109 "testing"
1110 "time"
@@ -25,7 +24,7 @@ func linkedRig(t *testing.T) (*mrRig, *usecase.IssueLinks, *usecase.Issues) {
2524 r.seed()
2625 _, err := r.store.Mutate(r.ctx, r.repo, func(repo *domain.Repo) (bool, error) { repo.Settings.Issues = domain.IssuesOwner; return true, nil })
2726 require.NoError(t, err)
28- issueStore := &linkIssueStore{IssueStore: r.store.Issues(), issues: make(map[int64]domain.Issue)}
27+ issueStore := r.store.Issues()
2928 links := usecase.NewIssueLinks(usecase.IssueLinksConfig{Meta: r.store, Issues: issueStore, MRs: r.store.MergeRequests(), Links: r.store.IssueLinks(), Git: r.git, Bus: r.bus, Clock: fixedClock{}})
3029 r.mrs = usecase.NewMergeRequests(usecase.MergeRequestsConfig{Meta: r.store, Store: r.store.MergeRequests(), Repos: r.git, Git: r.git, Bus: r.bus, Clock: fixedClock{}, Maint: r.sched, Links: links})
3130 issues := usecase.NewIssues(usecase.IssuesConfig{Meta: r.store, Store: issueStore, MergeRequests: r.store.MergeRequests(), Links: r.store.IssueLinks(), Bus: r.bus, Clock: fixedClock{}})
@@ -128,9 +127,35 @@ func TestIssueLinksMergeClosing(t *testing.T) {
128127 assert.Equal(t, mr.Number, got.ClosedByMergeRequest)
129128 assert.Equal(t, domain.IssueStateResolved, got.State)
130129 assert.Equal(t, 1, closedEvents)
130+ persisted, err := r.store.Issues().Get(r.ctx, r.repo, issue.Number)
131+ require.NoError(t, err)
132+ assert.Equal(t, got, persisted)
133+ history, err := r.store.Issues().ListEvents(r.ctx, r.repo, issue.Number)
134+ require.NoError(t, err)
135+ require.NotEmpty(t, history)
136+ last := history[len(history)-1]
137+ assert.Equal(t, domain.IssueEventResolved, last.Kind)
138+ assert.Equal(t, domain.AuthorName(root), last.Actor)
139+ assert.Equal(t, mr.Number, last.MergeRequest)
140+ page, err := r.store.Issues().List(r.ctx, r.repo, ports.ListIssuesParams{State: domain.IssueStateOpen})
141+ require.NoError(t, err)
142+ assert.Empty(t, page.Issues)
143+
131144 reopened, err := issues.Reopen(r.ctx, root, r.repo, issue.Number)
132145 require.NoError(t, err)
133146 assert.Zero(t, reopened.ClosedByMergeRequest)
147+ persisted, err = r.store.Issues().Get(r.ctx, r.repo, issue.Number)
148+ require.NoError(t, err)
149+ assert.Equal(t, domain.IssueStateOpen, persisted.State)
150+ assert.Zero(t, persisted.ClosedByMergeRequest)
151+ history, err = r.store.Issues().ListEvents(r.ctx, r.repo, issue.Number)
152+ require.NoError(t, err)
153+ assert.Equal(t, domain.IssueEventReopened, history[len(history)-1].Kind)
154+ page, err = r.store.Issues().List(r.ctx, r.repo, ports.ListIssuesParams{State: domain.IssueStateOpen})
155+ require.NoError(t, err)
156+ require.Len(t, page.Issues, 1)
157+ assert.Equal(t, persisted, page.Issues[0])
158+
134159 } else {
135160 assert.Zero(t, got.ClosedByMergeRequest)
136161 assert.Equal(t, 0, closedEvents)
@@ -270,36 +295,6 @@ func TestAutoMergeClosesLinkedIssue(t *testing.T) {
270295 assert.False(t, got.Issue.IsOpen())
271296 }
272297
273-// linkIssueStore isolates merge state writes from the independently developed
274-// kvstore resolved-state encoder. All other operations use the existing rig.
275-type linkIssueStore struct {
276- ports.IssueStore
277- mu sync.Mutex
278- issues map[int64]domain.Issue
279-}
280-
281-func (s *linkIssueStore) Get(ctx context.Context, repo domain.RepoName, n int64) (domain.Issue, error) {
282- s.mu.Lock()
283- defer s.mu.Unlock()
284- if issue, ok := s.issues[n]; ok {
285- return issue, nil
286- }
287- return s.IssueStore.Get(ctx, repo, n)
288-}
289-func (s *linkIssueStore) Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error {
290- s.mu.Lock()
291- defer s.mu.Unlock()
292- if issue.State == domain.IssueStateResolved {
293- s.issues[issue.Number] = issue
294- return nil
295- }
296- if _, ok := s.issues[issue.Number]; ok {
297- s.issues[issue.Number] = issue
298- return nil
299- }
300- return s.IssueStore.Update(ctx, issue, events...)
301-}
302-
303298 func TestMergeResolutionActorAndReferenceEvent(t *testing.T) {
304299 for _, state := range []domain.IssueState{domain.IssueStateOpen, domain.IssueStateClosed, domain.IssueStateResolved} {
305300 t.Run(string(state), func(t *testing.T) {
@@ -5,7 +5,6 @@ package usecase_test
5 import (5 import (
6 "context"6 "context"
7 "fmt"7 "fmt"
8- "sync"
9 "sync/atomic"8 "sync/atomic"
10 "testing"9 "testing"
11 "time"10 "time"
@@ -25,7 +24,7 @@ func linkedRig(t *testing.T) (*mrRig, *usecase.IssueLinks, *usecase.Issues) {
25 r.seed()24 r.seed()
26 _, err := r.store.Mutate(r.ctx, r.repo, func(repo *domain.Repo) (bool, error) { repo.Settings.Issues = domain.IssuesOwner; return true, nil })25 _, err := r.store.Mutate(r.ctx, r.repo, func(repo *domain.Repo) (bool, error) { repo.Settings.Issues = domain.IssuesOwner; return true, nil })
27 require.NoError(t, err)26 require.NoError(t, err)
28- issueStore := &linkIssueStore{IssueStore: r.store.Issues(), issues: make(map[int64]domain.Issue)}27+ issueStore := r.store.Issues()
29 links := usecase.NewIssueLinks(usecase.IssueLinksConfig{Meta: r.store, Issues: issueStore, MRs: r.store.MergeRequests(), Links: r.store.IssueLinks(), Git: r.git, Bus: r.bus, Clock: fixedClock{}})28 links := usecase.NewIssueLinks(usecase.IssueLinksConfig{Meta: r.store, Issues: issueStore, MRs: r.store.MergeRequests(), Links: r.store.IssueLinks(), Git: r.git, Bus: r.bus, Clock: fixedClock{}})
30 r.mrs = usecase.NewMergeRequests(usecase.MergeRequestsConfig{Meta: r.store, Store: r.store.MergeRequests(), Repos: r.git, Git: r.git, Bus: r.bus, Clock: fixedClock{}, Maint: r.sched, Links: links})29 r.mrs = usecase.NewMergeRequests(usecase.MergeRequestsConfig{Meta: r.store, Store: r.store.MergeRequests(), Repos: r.git, Git: r.git, Bus: r.bus, Clock: fixedClock{}, Maint: r.sched, Links: links})
31 issues := usecase.NewIssues(usecase.IssuesConfig{Meta: r.store, Store: issueStore, MergeRequests: r.store.MergeRequests(), Links: r.store.IssueLinks(), Bus: r.bus, Clock: fixedClock{}})30 issues := usecase.NewIssues(usecase.IssuesConfig{Meta: r.store, Store: issueStore, MergeRequests: r.store.MergeRequests(), Links: r.store.IssueLinks(), Bus: r.bus, Clock: fixedClock{}})
@@ -128,9 +127,35 @@ func TestIssueLinksMergeClosing(t *testing.T) {
128 assert.Equal(t, mr.Number, got.ClosedByMergeRequest)127 assert.Equal(t, mr.Number, got.ClosedByMergeRequest)
129 assert.Equal(t, domain.IssueStateResolved, got.State)128 assert.Equal(t, domain.IssueStateResolved, got.State)
130 assert.Equal(t, 1, closedEvents)129 assert.Equal(t, 1, closedEvents)
130+ persisted, err := r.store.Issues().Get(r.ctx, r.repo, issue.Number)
131+ require.NoError(t, err)
132+ assert.Equal(t, got, persisted)
133+ history, err := r.store.Issues().ListEvents(r.ctx, r.repo, issue.Number)
134+ require.NoError(t, err)
135+ require.NotEmpty(t, history)
136+ last := history[len(history)-1]
137+ assert.Equal(t, domain.IssueEventResolved, last.Kind)
138+ assert.Equal(t, domain.AuthorName(root), last.Actor)
139+ assert.Equal(t, mr.Number, last.MergeRequest)
140+ page, err := r.store.Issues().List(r.ctx, r.repo, ports.ListIssuesParams{State: domain.IssueStateOpen})
141+ require.NoError(t, err)
142+ assert.Empty(t, page.Issues)
143+
131 reopened, err := issues.Reopen(r.ctx, root, r.repo, issue.Number)144 reopened, err := issues.Reopen(r.ctx, root, r.repo, issue.Number)
132 require.NoError(t, err)145 require.NoError(t, err)
133 assert.Zero(t, reopened.ClosedByMergeRequest)146 assert.Zero(t, reopened.ClosedByMergeRequest)
147+ persisted, err = r.store.Issues().Get(r.ctx, r.repo, issue.Number)
148+ require.NoError(t, err)
149+ assert.Equal(t, domain.IssueStateOpen, persisted.State)
150+ assert.Zero(t, persisted.ClosedByMergeRequest)
151+ history, err = r.store.Issues().ListEvents(r.ctx, r.repo, issue.Number)
152+ require.NoError(t, err)
153+ assert.Equal(t, domain.IssueEventReopened, history[len(history)-1].Kind)
154+ page, err = r.store.Issues().List(r.ctx, r.repo, ports.ListIssuesParams{State: domain.IssueStateOpen})
155+ require.NoError(t, err)
156+ require.Len(t, page.Issues, 1)
157+ assert.Equal(t, persisted, page.Issues[0])
158+
134 } else {159 } else {
135 assert.Zero(t, got.ClosedByMergeRequest)160 assert.Zero(t, got.ClosedByMergeRequest)
136 assert.Equal(t, 0, closedEvents)161 assert.Equal(t, 0, closedEvents)
@@ -270,36 +295,6 @@ func TestAutoMergeClosesLinkedIssue(t *testing.T) {
270 assert.False(t, got.Issue.IsOpen())295 assert.False(t, got.Issue.IsOpen())
271 }296 }
272 297
273-// linkIssueStore isolates merge state writes from the independently developed
274-// kvstore resolved-state encoder. All other operations use the existing rig.
275-type linkIssueStore struct {
276- ports.IssueStore
277- mu sync.Mutex
278- issues map[int64]domain.Issue
279-}
280-
281-func (s *linkIssueStore) Get(ctx context.Context, repo domain.RepoName, n int64) (domain.Issue, error) {
282- s.mu.Lock()
283- defer s.mu.Unlock()
284- if issue, ok := s.issues[n]; ok {
285- return issue, nil
286- }
287- return s.IssueStore.Get(ctx, repo, n)
288-}
289-func (s *linkIssueStore) Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error {
290- s.mu.Lock()
291- defer s.mu.Unlock()
292- if issue.State == domain.IssueStateResolved {
293- s.issues[issue.Number] = issue
294- return nil
295- }
296- if _, ok := s.issues[issue.Number]; ok {
297- s.issues[issue.Number] = issue
298- return nil
299- }
300- return s.IssueStore.Update(ctx, issue, events...)
301-}
302-
303 func TestMergeResolutionActorAndReferenceEvent(t *testing.T) {298 func TestMergeResolutionActorAndReferenceEvent(t *testing.T) {
304 for _, state := range []domain.IssueState{domain.IssueStateOpen, domain.IssueStateClosed, domain.IssueStateResolved} {299 for _, state := range []domain.IssueState{domain.IssueStateOpen, domain.IssueStateClosed, domain.IssueStateResolved} {
305 t.Run(string(state), func(t *testing.T) {300 t.Run(string(state), func(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close