| 1 | package bot |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sync" |
| 6 | "testing" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // Guards pairingMu + the atomic savePairingFile: concurrent offerPairing |
| 11 | // dispatch goroutines used to load-modify-save pairing.json without a lock and |
| 12 | // overwrite each other's requests. Run with -race. |
| 13 | func TestCreateOrRefreshPairingRequestConcurrent(t *testing.T) { |
| 14 | t.Setenv("REASONIX_HOME", t.TempDir()) |
| 15 | cfg := PairingConfig{Enabled: true, RequestTTL: time.Hour, MaxPendingPerPlatform: 64} |
| 16 | |
| 17 | const workers = 8 |
| 18 | var wg sync.WaitGroup |
| 19 | errs := make(chan error, workers) |
| 20 | for i := 0; i < workers; i++ { |
| 21 | wg.Add(1) |
| 22 | go func(i int) { |
| 23 | defer wg.Done() |
| 24 | msg := InboundMessage{ |
| 25 | Platform: PlatformFeishu, |
| 26 | ChatType: ChatDM, |
| 27 | ChatID: fmt.Sprintf("chat-%d", i), |
| 28 | UserID: fmt.Sprintf("user-%d", i), |
| 29 | } |
| 30 | for j := 0; j < 5; j++ { |
| 31 | if _, _, err := CreateOrRefreshPairingRequest(msg, cfg); err != nil { |
| 32 | errs <- err |
| 33 | return |
| 34 | } |
| 35 | } |
| 36 | }(i) |
| 37 | } |
| 38 | wg.Wait() |
| 39 | close(errs) |
| 40 | for err := range errs { |
| 41 | t.Fatalf("concurrent pairing request failed: %v", err) |
| 42 | } |
| 43 | |
| 44 | reqs, err := ListPairingRequests() |
| 45 | if err != nil { |
| 46 | t.Fatalf("list pairing requests: %v", err) |
| 47 | } |
| 48 | if len(reqs) != workers { |
| 49 | t.Fatalf("pairing store lost concurrent writes: got %d requests, want %d", len(reqs), workers) |
| 50 | } |
| 51 | } |
| 52 |