fix: own comments trigger NEW
This commit is contained in:
@@ -35,6 +35,7 @@ type mutationOperation struct {
|
|||||||
PRID string `json:"pull_request_id"`
|
PRID string `json:"pull_request_id"`
|
||||||
ThreadID string `json:"thread_id,omitempty"`
|
ThreadID string `json:"thread_id,omitempty"`
|
||||||
Body string `json:"body,omitempty"`
|
Body string `json:"body,omitempty"`
|
||||||
|
ReplyID string `json:"reply_id,omitempty"`
|
||||||
Resolved bool `json:"resolved,omitempty"`
|
Resolved bool `json:"resolved,omitempty"`
|
||||||
Viewer string `json:"viewer,omitempty"`
|
Viewer string `json:"viewer,omitempty"`
|
||||||
Original PullRequestMetadata `json:"original,omitempty"`
|
Original PullRequestMetadata `json:"original,omitempty"`
|
||||||
@@ -372,9 +373,11 @@ func executeQueuedMutation(
|
|||||||
reason: "could not checkpoint the reply attempt", err: err,
|
reason: "could not checkpoint the reply attempt", err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := writer.ReplyToThread(ctx, operation.ThreadID, operation.Body); err != nil {
|
comment, err := writer.ReplyToThread(ctx, operation.ThreadID, operation.Body)
|
||||||
|
if err != nil {
|
||||||
return mutationReplayMsg{operation: operation, state: mutationReplayWaiting, details: details, err: err}
|
return mutationReplayMsg{operation: operation, state: mutationReplayWaiting, details: details, err: err}
|
||||||
}
|
}
|
||||||
|
operation.ReplyID = comment.ID
|
||||||
return verifying()
|
return verifying()
|
||||||
case mutationResolution:
|
case mutationResolution:
|
||||||
if thread == nil {
|
if thread == nil {
|
||||||
@@ -477,6 +480,9 @@ func queuedReplyPresent(details PRDetails, operation mutationOperation) bool {
|
|||||||
}
|
}
|
||||||
matches := 0
|
matches := 0
|
||||||
for _, comment := range thread.Comments {
|
for _, comment := range thread.Comments {
|
||||||
|
if operation.ReplyID != "" && comment.ID == operation.ReplyID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if comment.Body == operation.Body && strings.EqualFold(comment.Author, operation.Viewer) &&
|
if comment.Body == operation.Body && strings.EqualFold(comment.Author, operation.Viewer) &&
|
||||||
!comment.CreatedAt.Before(operation.EnqueuedAt.Add(-time.Minute)) {
|
!comment.CreatedAt.Before(operation.EnqueuedAt.Add(-time.Minute)) {
|
||||||
matches++
|
matches++
|
||||||
@@ -546,6 +552,10 @@ func (m App) projectQueuedMutations(details PRDetails) PRDetails {
|
|||||||
pendingID := "pending:" + operation.ID
|
pendingID := "pending:" + operation.ID
|
||||||
found := false
|
found := false
|
||||||
for index := range thread.Comments {
|
for index := range thread.Comments {
|
||||||
|
if queuedReplyMatchesComment(thread.Comments[index], operation) {
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
if thread.Comments[index].ID == pendingID {
|
if thread.Comments[index].ID == pendingID {
|
||||||
thread.Comments[index].Pending = mutationNeedsAttention(operation)
|
thread.Comments[index].Pending = mutationNeedsAttention(operation)
|
||||||
found = true
|
found = true
|
||||||
@@ -573,6 +583,15 @@ func (m App) projectQueuedMutations(details PRDetails) PRDetails {
|
|||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func queuedReplyMatchesComment(comment ReviewComment, operation mutationOperation) bool {
|
||||||
|
if operation.ReplyID != "" && comment.ID == operation.ReplyID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return comment.Body == operation.Body &&
|
||||||
|
strings.EqualFold(comment.Author, operation.Viewer) &&
|
||||||
|
!comment.CreatedAt.Before(operation.EnqueuedAt.Add(-time.Minute))
|
||||||
|
}
|
||||||
|
|
||||||
func mutationNeedsAttention(operation mutationOperation) bool {
|
func mutationNeedsAttention(operation mutationOperation) bool {
|
||||||
return operation.Blocked || operation.Unverified
|
return operation.Blocked || operation.Unverified
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,56 @@ func TestReplayReconcilesReplyBeforeAskingAboutAmbiguousDelivery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReplyProjectionUsesRemoteCommentWithoutTemporaryDuplicate(t *testing.T) {
|
||||||
|
store := loadMutationQueue("")
|
||||||
|
enqueued := time.Now().Add(-time.Second)
|
||||||
|
operation := mutationOperation{
|
||||||
|
ID: "reply", Kind: mutationReply, Owner: "o", Repository: "r", Number: 1,
|
||||||
|
ThreadID: "thread", Body: "body", Viewer: "me", EnqueuedAt: enqueued,
|
||||||
|
}
|
||||||
|
if err := store.add(operation); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := NewApp(nil, "o", "r", false, 50, time.Minute)
|
||||||
|
m.mutations = store
|
||||||
|
details := PRDetails{
|
||||||
|
PullRequest: PullRequest{Owner: "o", Repository: "r", Number: 1},
|
||||||
|
Threads: []ReviewThread{{ID: "thread", Comments: []ReviewComment{
|
||||||
|
{ID: "remote", Author: "me", Body: "body", CreatedAt: enqueued.Add(time.Second)},
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
projected := m.projectQueuedMutations(details)
|
||||||
|
if len(projected.Threads[0].Comments) != 1 || projected.Threads[0].Comments[0].ID != "remote" {
|
||||||
|
t.Fatalf("single remote reply was duplicated: %#v", projected.Threads[0].Comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
details.Threads[0].Comments = append(details.Threads[0].Comments, ReviewComment{
|
||||||
|
ID: "actual-duplicate", Author: "me", Body: "body", CreatedAt: enqueued.Add(2 * time.Second),
|
||||||
|
})
|
||||||
|
projected = m.projectQueuedMutations(details)
|
||||||
|
if len(projected.Threads[0].Comments) != 2 {
|
||||||
|
t.Fatalf("remote duplicates were not preserved exactly: %#v", projected.Threads[0].Comments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuccessfulReplyCheckpointsRemoteCommentID(t *testing.T) {
|
||||||
|
store := loadMutationQueue("")
|
||||||
|
operation := mutationOperation{
|
||||||
|
ID: "reply", Kind: mutationReply, Owner: "o", Repository: "r", Number: 1,
|
||||||
|
ThreadID: "thread", Body: "body", Viewer: "me", EnqueuedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := store.add(operation); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
details := PRDetails{Threads: []ReviewThread{{ID: "thread", ViewerCanReply: true}}}
|
||||||
|
result := executeQueuedMutation(context.Background(), &recordingService{}, store, operation, details)
|
||||||
|
stored, ok := store.front()
|
||||||
|
if result.state != mutationReplayVerifying || !ok || stored.ReplyID != "new-comment" {
|
||||||
|
t.Fatalf("reply verification identity was not checkpointed: result=%#v stored=%#v", result, stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSuccessfulResolutionWaitsForLiveVerification(t *testing.T) {
|
func TestSuccessfulResolutionWaitsForLiveVerification(t *testing.T) {
|
||||||
store := loadMutationQueue("")
|
store := loadMutationQueue("")
|
||||||
operation := mutationOperation{
|
operation := mutationOperation{
|
||||||
|
|||||||
30
tui.go
30
tui.go
@@ -1638,6 +1638,8 @@ func (m *App) trackThreadUpdates(details PRDetails) {
|
|||||||
state := m.readState.Data[prID]
|
state := m.readState.Data[prID]
|
||||||
if state.Threads == nil {
|
if state.Threads == nil {
|
||||||
state.Threads = make(map[string]bool)
|
state.Threads = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if state.Comments == nil {
|
||||||
state.Comments = make(map[string]bool)
|
state.Comments = make(map[string]bool)
|
||||||
}
|
}
|
||||||
if !state.Initialized {
|
if !state.Initialized {
|
||||||
@@ -1654,25 +1656,47 @@ func (m *App) trackThreadUpdates(details PRDetails) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
readStateChanged := false
|
||||||
for _, thread := range details.Threads {
|
for _, thread := range details.Threads {
|
||||||
threadIsNew := !state.Threads[thread.ID]
|
threadIsNew := !state.Threads[thread.ID]
|
||||||
updated := threadIsNew
|
updated := threadIsNew
|
||||||
if threadIsNew {
|
viewerAuthoredOnly := len(thread.Comments) > 0
|
||||||
m.newThreads[thread.ID] = true
|
|
||||||
}
|
|
||||||
for _, comment := range thread.Comments {
|
for _, comment := range thread.Comments {
|
||||||
|
viewerAuthored := details.ViewerLogin != "" &&
|
||||||
|
strings.EqualFold(comment.Author, details.ViewerLogin)
|
||||||
|
if viewerAuthored {
|
||||||
|
if !state.Comments[comment.ID] {
|
||||||
|
state.Comments[comment.ID] = true
|
||||||
|
readStateChanged = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewerAuthoredOnly = false
|
||||||
|
}
|
||||||
if !state.Comments[comment.ID] {
|
if !state.Comments[comment.ID] {
|
||||||
updated = true
|
updated = true
|
||||||
m.unreadComments[comment.ID] = true
|
m.unreadComments[comment.ID] = true
|
||||||
}
|
}
|
||||||
m.knownComments[comment.ID] = true
|
m.knownComments[comment.ID] = true
|
||||||
}
|
}
|
||||||
|
if threadIsNew && !viewerAuthoredOnly {
|
||||||
|
m.newThreads[thread.ID] = true
|
||||||
|
} else if threadIsNew {
|
||||||
|
state.Threads[thread.ID] = true
|
||||||
|
readStateChanged = true
|
||||||
|
updated = false
|
||||||
|
}
|
||||||
m.knownThreads[thread.ID] = true
|
m.knownThreads[thread.ID] = true
|
||||||
if updated {
|
if updated {
|
||||||
m.unreadThreads[thread.ID] = true
|
m.unreadThreads[thread.ID] = true
|
||||||
m.updatedThreads[thread.ID] = true
|
m.updatedThreads[thread.ID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if readStateChanged {
|
||||||
|
m.readState.Data[prID] = state
|
||||||
|
if err := m.readState.save(); err != nil {
|
||||||
|
m.recordHealth("read state", healthWarning, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
m.initializedPRs[prID] = true
|
m.initializedPRs[prID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
tui_test.go
33
tui_test.go
@@ -1752,6 +1752,39 @@ func TestPollingMarksNewThreadCommentsUnread(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPollingDoesNotMarkViewerReplyUnread(t *testing.T) {
|
||||||
|
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||||
|
m.screen = threadScreen
|
||||||
|
initial := PRDetails{
|
||||||
|
PullRequest: PullRequest{ID: "pr", Owner: "o", Repository: "r", Number: 1},
|
||||||
|
ViewerLogin: "me",
|
||||||
|
Threads: []ReviewThread{{
|
||||||
|
ID: "thread", Comments: []ReviewComment{{ID: "original", Author: "reviewer"}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
updated, _ := m.Update(detailsLoadedMsg{owner: "o", repo: "r", number: 1, details: initial})
|
||||||
|
m = updated.(App)
|
||||||
|
|
||||||
|
refreshed := initial
|
||||||
|
refreshed.Threads = []ReviewThread{{
|
||||||
|
ID: "thread", Comments: []ReviewComment{
|
||||||
|
{ID: "original", Author: "reviewer"},
|
||||||
|
{ID: "own-reply", Author: "ME", Body: "sent by me"},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
updated, _ = m.Update(detailsLoadedMsg{owner: "o", repo: "r", number: 1, details: refreshed})
|
||||||
|
m = updated.(App)
|
||||||
|
|
||||||
|
if m.unreadThreads["thread"] || m.unreadComments["own-reply"] || len(m.updatedThreads) != 0 {
|
||||||
|
t.Fatalf("viewer reply was marked new: threads=%v comments=%v updated=%v",
|
||||||
|
m.unreadThreads, m.unreadComments, m.updatedThreads)
|
||||||
|
}
|
||||||
|
state := m.readState.Data["pr"]
|
||||||
|
if !state.Comments["own-reply"] {
|
||||||
|
t.Fatal("viewer reply was not persisted as read")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolvingThreadMarksItRead(t *testing.T) {
|
func TestResolvingThreadMarksItRead(t *testing.T) {
|
||||||
service := &recordingService{}
|
service := &recordingService{}
|
||||||
m := NewApp(service, "o", "r", false, 50, time.Second)
|
m := NewApp(service, "o", "r", false, 50, time.Second)
|
||||||
|
|||||||
Reference in New Issue
Block a user