Files
kandev/apps/backend/internal/github/poller_branch_reconciliation_test.go
T
2026-09-14 13:59:28 +01:00

127 lines
4.2 KiB
Go

package github
import (
"context"
"testing"
"time"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
)
type watchIdentityProvider struct{ targets []TaskBranchInfo }
func (p *watchIdentityProvider) ListTasksNeedingPRWatch(context.Context) ([]TaskBranchInfo, error) {
return p.targets, nil
}
func (p *watchIdentityProvider) ResolveBranchForWatch(_ context.Context, w *PRWatch) string {
for _, target := range p.targets {
if target.SessionID == w.SessionID && target.RepositoryID == w.RepositoryID && target.Branch == w.Branch {
return target.Branch
}
}
return ""
}
// @covers AC-INTEGRATIONS-GITHUB-PR-DISCOVERY-001.1
// @covers AC-INTEGRATIONS-GITHUB-PR-DISCOVERY-001.4
func TestReconcileWatches_PreservesBranchTargetsAcrossCycles(t *testing.T) {
poller, svc, client, store := setupBatchedPollerTest(t)
ctx := context.Background()
seedTask(t, store, "owner", false)
provider := &watchIdentityProvider{targets: []TaskBranchInfo{
{WorkspaceID: testWorkspaceID, TaskID: "owner", SessionID: "member-session", RepositoryID: "repo1", Owner: "o", Repo: "r", Branch: "primary"},
{WorkspaceID: testWorkspaceID, TaskID: "owner", SessionID: "member-session", RepositoryID: "repo1", Owner: "o", Repo: "r", Branch: "secondary"},
{WorkspaceID: testWorkspaceID, TaskID: "owner", SessionID: "member-session", RepositoryID: "repo2", Owner: "o", Repo: "other", Branch: "other-branch"},
}}
poller.SetTaskBranchProvider(provider)
poller.reconcileWatches(ctx)
initial, err := store.ListActivePRWatches(ctx)
if err != nil {
t.Fatal(err)
}
if len(initial) != 3 {
t.Fatalf("initial watches = %d, want 3", len(initial))
}
// A numbered sibling must survive while the other branches still search.
for _, w := range initial {
if w.Branch == "primary" {
if err := store.UpdatePRWatchPRNumber(ctx, w.ID, 99); err != nil {
t.Fatal(err)
}
}
}
initial, err = store.ListActivePRWatches(ctx)
if err != nil {
t.Fatal(err)
}
assertWatchReconciliationStable(t, poller, store, initial)
assertSecondaryWatchDiscovery(t, svc, client, store)
}
func assertWatchReconciliationStable(t *testing.T, poller *Poller, store *Store, initial []*PRWatch) {
t.Helper()
ctx := context.Background()
for range 3 {
poller.reconcileWatches(ctx)
current, err := store.ListActivePRWatches(ctx)
if err != nil {
t.Fatal(err)
}
if len(current) != len(initial) {
t.Fatalf("watch count = %d, want %d", len(current), len(initial))
}
for _, before := range initial {
after, err := store.GetPRWatchBySessionRepoAndBranch(ctx, before.SessionID, before.RepositoryID, before.Branch)
if err != nil {
t.Fatal(err)
}
if after == nil || after.ID != before.ID || !after.UpdatedAt.Equal(before.UpdatedAt) {
t.Fatalf("watch %s recreated or rewritten: before=%+v after=%+v", before.Branch, before, after)
}
if before.Branch == "primary" && after.PRNumber != 99 {
t.Fatal("numbered sibling changed")
}
}
}
}
func assertSecondaryWatchDiscovery(t *testing.T, svc *Service, client *graphQLMockClient, store *Store) {
t.Helper()
ctx := context.Background()
secondary, err := store.GetPRWatchBySessionRepoAndBranch(ctx, "member-session", "repo1", "secondary")
if err != nil {
t.Fatal(err)
}
updated := make(chan *TaskPR, 1)
sub, err := svc.eventBus.Subscribe(events.GitHubTaskPRUpdated, func(_ context.Context, event *bus.Event) error {
if pr, ok := event.Data.(*TaskPR); ok {
updated <- pr
}
return nil
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
client.branchResponses = []string{branchAliasResponse(t, 1, map[int]string{0: openPRNode(7, "secondary", "alice")})}
if _, err := svc.SyncWorkspaceWatchesBatched(ctx, testWorkspaceID, []*PRWatch{secondary}); err != nil {
t.Fatal(err)
}
associated, err := store.GetTaskPR(ctx, "owner")
if err != nil {
t.Fatal(err)
}
if associated == nil || associated.PRNumber != 7 || associated.RepositoryID != "repo1" {
t.Fatalf("association = %+v", associated)
}
select {
case event := <-updated:
if event.TaskID != "owner" || event.PRNumber != 7 || event.RepositoryID != "repo1" {
t.Fatalf("event = %+v", event)
}
case <-time.After(time.Second):
t.Fatal("missing task PR event")
}
}