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") }