package spindle
import (
"context"
"slices"
"testing"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/org_tangled"
"tangled.org/core/spindle/db"
"tangled.org/core/spindle/models"
)
func ticketRecord(state string) map[string]any {
return map[string]any{
"$type": org_tangled.TrackTicketNSID,
"state": state,
"title": map[string]any{"$type": "org.tangled.markup.markdown#inline", "text": "fix the thing"},
"createdAt": "2026-02-01T00:00:00Z",
"targetBranch": "main",
}
}
const syncedTicket = "3kticket00001"
type ticketSync struct {
t *testing.T
env *ensureEnv
repo *db.Repo
}
func newTicketSync(t *testing.T) *ticketSync {
env := newEnsureEnv(t)
env.serve("repo")
return &ticketSync{t: t, env: env, repo: env.ensure(t)}
}
func (s *ticketSync) put(state string, editor syntax.DID) {
s.env.knot.putVersion(testRepoDid, org_tangled.TrackTicketNSID, syncedTicket, editor, ticketRecord(state))
}
func (s *ticketSync) sync() error {
_, _, err := s.env.s.syncPullTicket(context.Background(), s.env.k, s.repo, syncedTicket)
return err
}
func (s *ticketSync) mustSync() {
s.t.Helper()
if err := s.sync(); err != nil {
s.t.Fatal(err)
}
}
func (s *ticketSync) events() []string {
s.t.Helper()
due, err := s.env.s.db.DuePullEvents(testRepoDid, time.Now(), 100)
if err != nil {
s.t.Fatal(err)
}
var out []string
for _, ev := range due {
out = append(out, string(ev.Event)+" by "+ev.Sender.String())
}
return out
}
func TestEveryStateChangeBetweenTwoSyncsFiresInOrder(t *testing.T) {
s := newTicketSync(t)
s.put("open", testBob)
s.mustSync()
s.put("closed", testCarol)
s.put("open", testBob)
s.mustSync()
want := []string{
string(models.WebhookEventPullRequestClosed) + " by " + testCarol.String(),
string(models.WebhookEventPullRequestReopened) + " by " + testBob.String(),
}
if got := s.events(); !slices.Equal(got, want) {
t.Fatalf("pull events = %v, want %v", got, want)
}
s.mustSync()
if got := s.events(); len(got) != 2 {
t.Fatalf("a sync with nothing new left events %v, want the same two", got)
}
}
func TestAFailedVersionListDoesNotSkipTheStatesInBetween(t *testing.T) {
s := newTicketSync(t)
s.put("open", testBob)
s.mustSync()
s.put("closed", testCarol)
s.put("open", testBob)
s.env.knot.failVersions = true
if err := s.sync(); err == nil {
t.Fatal("a sync whose version list failed reported success, so its mark would be dropped")
}
s.env.knot.failVersions = false
s.mustSync()
if got := s.events(); len(got) != 2 {
t.Fatalf("pull events = %v, want the closed and the reopened that the failed sync must not skip", got)
}
}