package main import ( "context" "sync" "testing" "time" ) // TestConcurrentComments_AllRetained — 100 goroutines add comments to the // same post. All should land, no path collisions, no lost writes. func TestConcurrentComments_AllRetained(t *testing.T) { s, _, _ := newTestStore(t) cp := &CreatePost{AuthorID: "scott", Body: "race test"} must(t, Submit(context.Background(), s, cp)) const N = 100 var wg sync.WaitGroup errCh := make(chan error, N) for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() m := &AddComment{PostID: cp.ResolvedPostID(), AuthorID: "hermes", Body: "concurrent"} errCh <- Submit(context.Background(), s, m) }() } wg.Wait() close(errCh) for err := range errCh { if err != nil { t.Fatalf("concurrent submit: %v", err) } } comments := s.ListComments(cp.ResolvedPostID()) if len(comments) != N { t.Fatalf("expected %d comments, got %d", N, len(comments)) } // All paths must be unique and parseable. seen := make(map[string]bool, N) for _, c := range comments { if seen[c.Path] { t.Fatalf("duplicate path: %s", c.Path) } seen[c.Path] = true } } // TestBumpOrder_AfterConcurrentComments — concurrent comments land in some // order, but the final post bump_at should be the time of the LAST comment. // With the per-author coarse lock, mutations serialize, so the order is // deterministic given the goroutine schedule. We don't assert exact order // (that would be flaky); we assert that the final bump_at is the latest. func TestBumpOrder_AfterConcurrentComments(t *testing.T) { s, clock, _ := newTestStore(t) cp := &CreatePost{AuthorID: "scott", Body: "bump order"} must(t, Submit(context.Background(), s, cp)) initialBump := s.posts[cp.ResolvedPostID()].BumpedAt const N = 10 var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() clock.Advance(time.Second) m := &AddComment{PostID: cp.ResolvedPostID(), AuthorID: "hermes", Body: "x"} _ = Submit(context.Background(), s, m) }() } wg.Wait() got, _ := s.GetPost(cp.ResolvedPostID()) if !got.BumpedAt.After(initialBump) { t.Fatalf("post should have been bumped past %v, got %v", initialBump, got.BumpedAt) } }