Files
diple/mutation_queue_test.go

215 lines
8.8 KiB
Go

package main
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"time"
)
func TestMutationQueuePersistsFIFOAndCapturedGates(t *testing.T) {
path := filepath.Join(t.TempDir(), "mutation-queue.json")
store := loadMutationQueue(path)
first := mutationOperation{
ID: "first", Kind: mutationReply, Owner: "o", Repository: "r", Number: 1,
ThreadID: "thread", Body: "reply", ThreadCanReply: true,
Permissions: ViewerPermissions{CanReplyAny: true}, EnqueuedAt: time.Now(),
}
second := mutationOperation{
ID: "second", Kind: mutationResolution, Owner: "o", Repository: "r", Number: 1,
ThreadID: "thread", Resolved: true, ThreadCanResolve: true, EnqueuedAt: time.Now(),
}
if err := store.add(first); err != nil {
t.Fatal(err)
}
if err := store.add(second); err != nil {
t.Fatal(err)
}
reloaded := loadMutationQueue(path)
operations := reloaded.list()
if len(operations) != 2 || operations[0].ID != "first" || operations[1].ID != "second" ||
!operations[0].ThreadCanReply || !operations[0].Permissions.CanReplyAny {
t.Fatalf("reloaded queue = %#v", operations)
}
}
func TestCorruptMutationQueueRefusesToOverwriteUserData(t *testing.T) {
path := filepath.Join(t.TempDir(), "mutation-queue.json")
if err := atomicWriteJSON(path, map[string]any{"broken": true}, 0o600); err != nil {
t.Fatal(err)
}
store := loadMutationQueue(path)
if store.loadErr == nil {
t.Fatal("corrupt queue was accepted")
}
if err := store.add(mutationOperation{Kind: mutationReply}); err == nil {
t.Fatal("corrupt queue was overwritten by a new mutation")
}
}
func TestQueuedMutationsProjectWithoutChangingSnapshot(t *testing.T) {
store := loadMutationQueue("")
now := time.Now()
for _, operation := range []mutationOperation{
{ID: "reply", Kind: mutationReply, Owner: "o", Repository: "r", Number: 1,
ThreadID: "thread", Body: "pending body", Viewer: "me", EnqueuedAt: now},
{ID: "resolve", Kind: mutationResolution, Owner: "o", Repository: "r", Number: 1,
ThreadID: "thread", Resolved: true, EnqueuedAt: now.Add(time.Second)},
{ID: "edit", Kind: mutationPREdit, Owner: "o", Repository: "r", Number: 1,
Update: PullRequestMetadata{Title: "queued", Body: "body", BaseRef: "next",
Reviewers: []string{"reviewer"}, Assignees: []string{"assignee"}}, EnqueuedAt: now},
} {
if err := store.add(operation); err != nil {
t.Fatal(err)
}
}
m := NewApp(nil, "o", "r", false, 50, time.Minute)
m.mutations = store
snapshot := PRDetails{
PullRequest: PullRequest{Owner: "o", Repository: "r", Number: 1, Title: "remote"},
Threads: []ReviewThread{{ID: "thread"}},
}
projected := m.projectQueuedMutations(snapshot)
if snapshot.Title != "remote" || snapshot.Threads[0].IsResolved || len(snapshot.Threads[0].Comments) != 0 {
t.Fatalf("source snapshot was mutated: %#v", snapshot)
}
if projected.Title != "queued" || !projected.Pending || !projected.Threads[0].IsResolved ||
!projected.Threads[0].Pending || len(projected.Threads[0].Comments) != 1 ||
!projected.Threads[0].Comments[0].Pending {
t.Fatalf("projection = %#v", projected)
}
}
func TestCachedGrantedPermissionQueuesReply(t *testing.T) {
store := loadMutationQueue("")
settings := defaultAppSettings()
settings.Mutations = store
m := NewAppWithSettings(&recordingService{}, "o", "r", false, 50, time.Minute, settings)
m.loading, m.screen = false, threadScreen
m.details = PRDetails{
PullRequest: PullRequest{ID: "pr", Owner: "o", Repository: "r", Number: 1}, FromCache: true,
ViewerLogin: "me", Permissions: ViewerPermissions{CanReplyAny: true},
Threads: []ReviewThread{{ID: "thread", ViewerCanReply: true}},
}
m.writeMode, m.writeThreadID, m.replyDraft = writeReplyBusy, "thread", "offline reply"
message := m.submitReply()()
updated, command := m.Update(message)
m = updated.(App)
if store.count() != 1 || m.writeMode != writeNone ||
len(m.details.Threads[0].Comments) != 1 || !m.details.Threads[0].Comments[0].Pending {
t.Fatalf("queued cached reply: count=%d mode=%d details=%#v command=%v",
store.count(), m.writeMode, m.details, command)
}
}
func TestReplayChecksLivePermissionBeforeMutation(t *testing.T) {
service := &recordingService{}
store := loadMutationQueue("")
operation := mutationOperation{
ID: "reply", Kind: mutationReply, Owner: "o", Repository: "r", Number: 1,
ThreadID: "thread", Body: "body", Viewer: "me", ThreadCanReply: true, EnqueuedAt: time.Now(),
}
if err := store.add(operation); err != nil {
t.Fatal(err)
}
details := PRDetails{
PullRequest: PullRequest{Owner: "o", Repository: "r", Number: 1},
Threads: []ReviewThread{{ID: "thread", ViewerCanReply: false}},
}
result := executeQueuedMutation(context.Background(), service, store, operation, details)
if result.state != mutationReplayBlocked || service.writeBody != "" ||
!strings.Contains(result.reason, "no longer grants reply permission") {
t.Fatalf("permission replay result = %#v service=%#v", result, service)
}
}
func TestReplayReconcilesReplyBeforeAskingAboutAmbiguousDelivery(t *testing.T) {
store := loadMutationQueue("")
enqueued := time.Now().Add(-time.Minute)
operation := mutationOperation{
ID: "reply", Kind: mutationReply, Owner: "o", Repository: "r", Number: 1,
ThreadID: "thread", Body: "body", Viewer: "me", Attempted: true, EnqueuedAt: enqueued,
}
details := PRDetails{Threads: []ReviewThread{{
ID: "thread", ViewerCanReply: true,
Comments: []ReviewComment{{Author: "me", Body: "body", CreatedAt: enqueued.Add(time.Second)}},
}}}
result := executeQueuedMutation(context.Background(), &recordingService{}, store, operation, details)
if result.state != mutationReplayApplied {
t.Fatalf("reconciled reply = %#v", result)
}
details.Threads[0].Comments = append(details.Threads[0].Comments, details.Threads[0].Comments[0])
result = executeQueuedMutation(context.Background(), &recordingService{}, store, operation, details)
if result.state != mutationReplayBlocked || !result.operation.Ambiguous {
t.Fatalf("duplicate matching replies were not treated as ambiguous: %#v", result)
}
details.Threads[0].Comments = nil
result = executeQueuedMutation(context.Background(), &recordingService{}, store, operation, details)
if result.state != mutationReplayBlocked || !result.operation.Ambiguous {
t.Fatalf("ambiguous reply = %#v", result)
}
}
func TestQueuedPREditThreeWayMergeOnlyBlocksConflictingFields(t *testing.T) {
base := PullRequestMetadata{Title: "old", Body: "old body", BaseRef: "main"}
desired := base
desired.Title = "queued title"
remote := base
remote.Body = "remote body"
merged, conflict := rebaseQueuedPREdit(base, desired, remote)
if conflict != "" || merged.Title != "queued title" || merged.Body != "remote body" {
t.Fatalf("non-conflicting merge = %#v conflict=%q", merged, conflict)
}
remote.Title = "remote title"
_, conflict = rebaseQueuedPREdit(base, desired, remote)
if conflict != "title" {
t.Fatalf("conflict = %q, want title", conflict)
}
}
func TestBlockedPREditCanBeReviewedAndReplacedInPlace(t *testing.T) {
store := loadMutationQueue("")
operation := mutationOperation{
ID: "edit", Kind: mutationPREdit, Owner: "o", Repository: "r", Number: 1, PRID: "pr",
Original: PullRequestMetadata{Title: "old", Body: "body", BaseRef: "main"},
Update: PullRequestMetadata{Title: "queued", Body: "body", BaseRef: "main"},
Blocked: true, LastError: "title changed", EnqueuedAt: time.Now(),
}
if err := store.add(operation); err != nil {
t.Fatal(err)
}
settings := defaultAppSettings()
settings.Mutations = store
m := NewAppWithSettings(&recordingPRService{}, "o", "r", false, 50, time.Minute, settings)
m.loading, m.blockedMutation, m.writeMode = false, &operation, writeQueueBlocked
m.blockedMutationDetails = PRDetails{
PullRequest: PullRequest{ID: "pr", Owner: "o", Repository: "r", Number: 1, Title: "remote"},
Body: "body", BaseRef: "main", Permissions: ViewerPermissions{CanUpdatePR: true},
}
command := m.resolveBlockedMutation(1)
if command == nil || m.writeMode != writePREdit || m.editingMutationID != "edit" ||
m.prEditEditors[prEditTitleField].Text != "queued" || m.prEditOriginal.Title != "remote" {
t.Fatalf("reviewed edit mode=%d id=%q title=%q original=%#v command=%v",
m.writeMode, m.editingMutationID, m.prEditEditors[prEditTitleField].Text, m.prEditOriginal, command)
}
m.prEditEditors[prEditTitleField].Text = "reconciled"
message := m.submitPREdit()()
if queued, ok := message.(mutationQueuedMsg); !ok || queued.err != nil {
t.Fatalf("replacement message = %#v", message)
}
replaced, ok := store.front()
if !ok || store.count() != 1 || replaced.ID != "edit" || replaced.Blocked ||
replaced.Original.Title != "remote" || replaced.Update.Title != "reconciled" {
t.Fatalf("replaced operation = %#v", replaced)
}
}
type failingReplyService struct{ recordingService }
func (s *failingReplyService) ReplyToThread(context.Context, string, string) (ReviewComment, error) {
return ReviewComment{}, errors.New("connection lost")
}