package main import ( "sync" "testing" ) // TestSingleFlightClaim documents the per-(room,thread) single-flight invariant the // async refactor relies on: at most one generation per conversation at a time, the claim // is independent per (room,thread), and a release re-arms only that conversation. // handleEvent takes this claim synchronously in transaction order, so the FIRST message // for a conversation wins and later ones are dropped until release (never the reverse). func TestSingleFlightClaim(t *testing.T) { b := &Bot{inflight: make(map[string]map[string]bool)} if !b.tryClaim("!a", "") { t.Fatal("first claim on (!a, main) should win") } if b.tryClaim("!a", "") { t.Fatal("second claim on (!a, main) must fail while in flight") } // A DIFFERENT thread in the SAME room must claim independently — the whole point of // per-(room,thread) single-flight: a slow answer in one conversation cannot block // another conversation in the same room. if !b.tryClaim("!a", "$root1") { t.Fatal("a different thread in the same room must claim independently") } if b.tryClaim("!a", "$root1") { t.Fatal("second claim on (!a, $root1) must fail while in flight") } if !b.tryClaim("!b", "") { t.Fatal("a different room must claim independently") } b.release("!a", "") if !b.tryClaim("!a", "") { t.Fatal("after release (!a, main) must be claimable again") } // Releasing the main timeline must NOT free the thread's claim. if b.tryClaim("!a", "$root1") { t.Fatal("releasing (!a, main) must not free (!a, $root1)") } } // TestSingleFlightClaimExactlyOneWinner runs many goroutines racing for the same // conversation and asserts EXACTLY ONE wins — the property that prevents two concurrent // generations (double xAI spend) for one conversation. It also races two DIFFERENT // threads of one room together and asserts each has its own single winner, proving the // claim is independent per (room,thread), not per room. Run under -race. func TestSingleFlightClaimExactlyOneWinner(t *testing.T) { b := &Bot{inflight: make(map[string]map[string]bool)} const n = 64 var sameWins, threadAWins, threadBWins int64 var mu sync.Mutex var wg sync.WaitGroup wg.Add(n * 3) for i := 0; i < n; i++ { go func() { defer wg.Done() if b.tryClaim("!room", "$same") { mu.Lock() sameWins++ mu.Unlock() } }() go func() { defer wg.Done() if b.tryClaim("!room", "$a") { mu.Lock() threadAWins++ mu.Unlock() } }() go func() { defer wg.Done() if b.tryClaim("!room", "$b") { mu.Lock() threadBWins++ mu.Unlock() } }() } wg.Wait() if sameWins != 1 { t.Fatalf("exactly one goroutine must win (!room, $same), got %d", sameWins) } if threadAWins != 1 { t.Fatalf("exactly one goroutine must win (!room, $a), got %d", threadAWins) } if threadBWins != 1 { t.Fatalf("exactly one goroutine must win (!room, $b), got %d", threadBWins) } } // TestLRUSetConcurrentAddOnce asserts the dedup set's check-and-insert is atomic: // with many goroutines racing on the same id, Add returns true exactly once. This is // the in-memory half of markSeen, now called from concurrent per-room goroutines. // Run under -race. func TestLRUSetConcurrentAddOnce(t *testing.T) { s := newLRUSet(1000) const n = 64 var trues int64 var mu sync.Mutex var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() if s.Add("$evt") { mu.Lock() trues++ mu.Unlock() } }() } wg.Wait() if trues != 1 { t.Fatalf("Add must return true exactly once for one id, got %d", trues) } }