A community based topic aggregation platform built on atproto

test: add test data generation scripts for comment threads

Add two new scripts for generating realistic test data:

- generate_deep_thread.go: Creates deeply nested comment threads (100 levels)
for testing threading logic, depth limits, and performance

- generate_nba_comments.go: Generates NBA-themed comments with realistic
basketball discussion content for UX testing and demos

Both scripts:
- Insert directly into PostgreSQL (bypassing Jetstream for speed)
- Create realistic comment trees with varied content
- Useful for stress testing, performance validation, and demos

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+570
+218
scripts/generate_deep_thread.go
···
··· 1 + package main 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "log" 7 + "math/rand" 8 + "time" 9 + 10 + _ "github.com/lib/pq" 11 + ) 12 + 13 + const ( 14 + postURI = "at://did:plc:hcuo3qx2lr7h7dquusbeobht/social.coves.community.post/3m56mowhbuk22" 15 + postCID = "bafyreibml4midgt7ojq7dnabnku5ikzro4erfvdux6mmiqeat7pci2gy4u" 16 + communityDID = "did:plc:hcuo3qx2lr7h7dquusbeobht" 17 + ) 18 + 19 + type User struct { 20 + DID string 21 + Handle string 22 + Name string 23 + } 24 + 25 + type Comment struct { 26 + URI string 27 + CID string 28 + RKey string 29 + DID string 30 + RootURI string 31 + RootCID string 32 + ParentURI string 33 + ParentCID string 34 + Content string 35 + CreatedAt time.Time 36 + } 37 + 38 + // Escalating conversation between two users 39 + var deepThreadConversation = []string{ 40 + "Wait, I just realized - if they both get suspended for this, their fantasy managers are SCREWED 😂", 41 + "Bro imagine being in a league where you have BOTH Duren brothers and they both get suspended for fighting EACH OTHER", 42 + "That's actually hilarious. 'Dear commissioner, my players got suspended for fighting... with each other'", 43 + "The fantasy implications are wild. Do you get negative points for your players fighting your other players? 🤔", 44 + "New fantasy category: Family Feuds. Duren brothers leading the league in FFD (Family Fight Disqualifications)", 45 + "I'm dying 💀 FFD should absolutely be a stat. The Morris twins would've been unstoppable in that category", 46 + "Don't forget the Plumlees! Those boys used to scrap in college practices. FFD Hall of Famers", 47 + "Okay but serious question: has there EVER been brothers fighting each other in an NBA game before this? This has to be a first", 48 + "I've been watching the NBA for 30 years and I can't think of a single time. This might genuinely be historic family beef", 49 + "So we're witnessing NBA history right now. Not the good kind, but history nonetheless. Their mom is SO proud 😂", 50 + } 51 + 52 + var userHandles = []string{ 53 + "deep_thread_guy_1.bsky.social", 54 + "deep_thread_guy_2.bsky.social", 55 + } 56 + 57 + func generateTID() string { 58 + now := time.Now().UnixMicro() 59 + return fmt.Sprintf("%d%04d", now, rand.Intn(10000)) 60 + } 61 + 62 + func createUser(db *sql.DB, handle string, idx int) (*User, error) { 63 + did := fmt.Sprintf("did:plc:deepthread%d%d", time.Now().Unix(), idx) 64 + user := &User{ 65 + DID: did, 66 + Handle: handle, 67 + Name: handle, 68 + } 69 + 70 + query := ` 71 + INSERT INTO users (did, handle, pds_url, created_at, updated_at) 72 + VALUES ($1, $2, $3, NOW(), NOW()) 73 + ON CONFLICT (did) DO NOTHING 74 + ` 75 + 76 + _, err := db.Exec(query, user.DID, user.Handle, "http://localhost:3001") 77 + if err != nil { 78 + return nil, fmt.Errorf("failed to create user: %w", err) 79 + } 80 + 81 + log.Printf("Created user: %s (%s)", user.Handle, user.DID) 82 + return user, nil 83 + } 84 + 85 + func createComment(db *sql.DB, user *User, content, parentURI, parentCID string, createdAt time.Time) (*Comment, error) { 86 + rkey := generateTID() 87 + uri := fmt.Sprintf("at://%s/social.coves.feed.comment/%s", user.DID, rkey) 88 + cid := fmt.Sprintf("bafy%s", rkey) 89 + 90 + comment := &Comment{ 91 + URI: uri, 92 + CID: cid, 93 + RKey: rkey, 94 + DID: user.DID, 95 + RootURI: postURI, 96 + RootCID: postCID, 97 + ParentURI: parentURI, 98 + ParentCID: parentCID, 99 + Content: content, 100 + CreatedAt: createdAt, 101 + } 102 + 103 + query := ` 104 + INSERT INTO comments ( 105 + uri, cid, rkey, commenter_did, root_uri, root_cid, 106 + parent_uri, parent_cid, content, created_at, indexed_at 107 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) 108 + ON CONFLICT (uri) DO NOTHING 109 + RETURNING id 110 + ` 111 + 112 + var id int64 113 + err := db.QueryRow(query, 114 + comment.URI, comment.CID, comment.RKey, comment.DID, 115 + comment.RootURI, comment.RootCID, comment.ParentURI, comment.ParentCID, 116 + comment.Content, comment.CreatedAt, 117 + ).Scan(&id) 118 + if err != nil { 119 + return nil, fmt.Errorf("failed to create comment: %w", err) 120 + } 121 + 122 + log.Printf("Level %d: %s", getCurrentLevel(parentURI), content) 123 + return comment, nil 124 + } 125 + 126 + func getCurrentLevel(parentURI string) int { 127 + if parentURI == postURI { 128 + return 1 129 + } 130 + // Count how many times we've nested (rough estimate) 131 + return 2 // Will be incremented as we go 132 + } 133 + 134 + func updateCommentCount(db *sql.DB, parentURI string, isPost bool) error { 135 + if isPost { 136 + _, err := db.Exec(` 137 + UPDATE posts 138 + SET comment_count = comment_count + 1 139 + WHERE uri = $1 140 + `, parentURI) 141 + return err 142 + } 143 + 144 + _, err := db.Exec(` 145 + UPDATE comments 146 + SET reply_count = reply_count + 1 147 + WHERE uri = $1 148 + `, parentURI) 149 + return err 150 + } 151 + 152 + func main() { 153 + dbURL := "postgres://dev_user:dev_password@localhost:5435/coves_dev?sslmode=disable" 154 + db, err := sql.Open("postgres", dbURL) 155 + if err != nil { 156 + log.Fatalf("Failed to connect to database: %v", err) 157 + } 158 + defer db.Close() 159 + 160 + if err := db.Ping(); err != nil { 161 + log.Fatalf("Failed to ping database: %v", err) 162 + } 163 + 164 + log.Println("Connected to database successfully!") 165 + log.Println("Creating 10-level deep comment thread...") 166 + 167 + rand.Seed(time.Now().UnixNano()) 168 + 169 + // Create two users who will have the back-and-forth 170 + user1, err := createUser(db, userHandles[0], 1) 171 + if err != nil { 172 + log.Fatalf("Failed to create user 1: %v", err) 173 + } 174 + 175 + user2, err := createUser(db, userHandles[1], 2) 176 + if err != nil { 177 + log.Fatalf("Failed to create user 2: %v", err) 178 + } 179 + 180 + baseTime := time.Now().Add(-30 * time.Minute) 181 + 182 + // Create the 10-level deep thread 183 + parentURI := postURI 184 + parentCID := postCID 185 + isPost := true 186 + 187 + for i, content := range deepThreadConversation { 188 + // Alternate between users 189 + user := user1 190 + if i%2 == 1 { 191 + user = user2 192 + } 193 + 194 + createdAt := baseTime.Add(time.Duration(i*2) * time.Minute) 195 + 196 + comment, err := createComment(db, user, content, parentURI, parentCID, createdAt) 197 + if err != nil { 198 + log.Fatalf("Failed to create comment at level %d: %v", i+1, err) 199 + } 200 + 201 + // Update parent's reply count 202 + if err := updateCommentCount(db, parentURI, isPost); err != nil { 203 + log.Printf("Warning: Failed to update comment count: %v", err) 204 + } 205 + 206 + // Set this comment as the parent for the next iteration 207 + parentURI = comment.URI 208 + parentCID = comment.CID 209 + isPost = false 210 + 211 + time.Sleep(10 * time.Millisecond) 212 + } 213 + 214 + log.Println("\n=== Summary ===") 215 + log.Printf("Created 10-level deep comment thread") 216 + log.Printf("Thread participants: %s and %s", user1.Handle, user2.Handle) 217 + log.Println("Done! Check the NBACentral post for the deep thread.") 218 + }
+352
scripts/generate_nba_comments.go
···
··· 1 + package main 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "log" 7 + "math/rand" 8 + "time" 9 + 10 + _ "github.com/lib/pq" 11 + ) 12 + 13 + // Post URI for "Your son don't wanna be here..." NBACentral post 14 + // at://did:plc:hcuo3qx2lr7h7dquusbeobht/social.coves.community.post/3m56mowhbuk22 15 + 16 + const ( 17 + postURI = "at://did:plc:hcuo3qx2lr7h7dquusbeobht/social.coves.community.post/3m56mowhbuk22" 18 + postCID = "bafyreibml4midgt7ojq7dnabnku5ikzro4erfvdux6mmiqeat7pci2gy4u" 19 + communityDID = "did:plc:hcuo3qx2lr7h7dquusbeobht" 20 + ) 21 + 22 + type User struct { 23 + DID string 24 + Handle string 25 + Name string 26 + } 27 + 28 + type Comment struct { 29 + URI string 30 + CID string 31 + RKey string 32 + DID string 33 + RootURI string 34 + RootCID string 35 + ParentURI string 36 + ParentCID string 37 + Content string 38 + CreatedAt time.Time 39 + } 40 + 41 + var userNames = []string{ 42 + "lakers_fan_23", "pistons_nation", "nba_historian", "hoops_enthusiast", 43 + "detroit_pride", "basketball_iq", "courtside_view", "rim_protector", 44 + "three_point_specialist", "paint_beast", "fast_break_fan", "clutch_time", 45 + "triple_double_king", "defense_wins", "small_ball_era", "old_school_hoops", 46 + "draft_expert", "salary_cap_guru", "trade_machine", "basketball_analytics", 47 + "box_score_reader", "eye_test_guy", "film_room_analyst", "player_development", 48 + "hometown_hero", "bandwagon_fan", "loyal_since_day_one", "casual_viewer", 49 + "die_hard_supporter", "armchair_coach", "nbatv_addict", "league_pass_subscriber", 50 + } 51 + 52 + var topLevelComments = []string{ 53 + "Imagine having to explain to your mom at Thanksgiving that you got ejected for fighting your brother 💀", 54 + "Mrs. Duren watching this at home like 'I didn't raise y'all like this'", 55 + "Their mom is somewhere absolutely LIVID right now. Both of them getting the belt when they get home", 56 + "This is the most expensive sibling rivalry in history lmao", 57 + "Jalen really said 'I've been whooping your ass since we were kids, what makes you think tonight's different' 😂", 58 + "Ausar thought the NBA would protect him from his older brother. He thought wrong.", 59 + "The trash talk must have been PERSONAL. That's years of sibling beef coming out", 60 + "Family group chat is gonna be awkward after this one", 61 + "Their parents spent 18 years breaking up fights just for it to happen on national TV", 62 + "This is what happens when little bro thinks he's tough now that he's in the league", 63 + "Jalen's been dunking on Ausar in the driveway for years, this was just another Tuesday for him", 64 + "The fact that they're both in the league and THIS is how they settle it 💀💀💀", 65 + "Ausar: 'I'm in the NBA now, I'm not scared of you anymore' - Jalen: 'BET'", 66 + "Mom definitely called both of them after the game. Neither one answered lol", 67 + "This is the content I pay League Pass for. Brothers getting into it on the court is peak entertainment", 68 + "Thanksgiving dinner is about to be TENSE in the Duren/Thompson household", 69 + "Little brother energy vs Big brother authority. Tale as old as time", 70 + "The refs trying to break them up like 'Sir that's your BROTHER'", 71 + "Jalen been waiting for this moment since Ausar got drafted", 72 + "Both of them getting fined and their mom making them split the cost 😂", 73 + "This brings me back to fighting my brother over the last piece of pizza. Just at a much higher tax bracket", 74 + "The Pistons and Rockets staff trying to separate them: 'Guys we have practice tomorrow!'", 75 + "Ausar finally tall enough to talk back and chose violence", 76 + "Their dad watching like 'At least wait til you're both All-Stars before embarrassing the family'", 77 + "This is what decades of 'Mom said it's my turn on the Xbox' leads to", 78 + } 79 + 80 + var replyComments = []string{ 81 + "LMAOOO facts, mom's not playing", 82 + "Bro I'm crying at this visual 😂😂😂", 83 + "This is the one right here 💀", 84 + "Thanksgiving about to be SILENT", 85 + "You know their dad had flashbacks to breaking up driveway fights", 86 + "The family group chat IS ON FIRE right now I guarantee it", 87 + "Little bro syndrome is real and Ausar has it BAD", 88 + "Big facts. Jalen been the big brother his whole life, NBA don't change that", 89 + "Mom's gonna make them hug it out before Christmas I'm calling it now", 90 + "This comment wins 😂😂😂", 91 + "I need the full footage of what was said because it had to be PERSONAL", 92 + "Years of sibling rivalry just exploded on NBA hardwood", 93 + "The refs were so confused trying to separate family members 💀", 94 + "Both of them getting the 'I'm disappointed' text from mom", 95 + "Ausar thought NBA money meant he was safe. Nope.", 96 + "Jalen's been waiting to humble him since draft night", 97 + "This is exactly what their parents warned them about lmao", 98 + "The fine money coming out of their allowance fr fr", 99 + "Peak sibling behavior. I respect it.", 100 + "Someone check on Mrs. Duren she's probably stress eating rn", 101 + } 102 + 103 + var deepReplyComments = []string{ 104 + "And you KNOW mom's taking both their sides AND neither side at the same time", 105 + "Family dynamics don't stop just cause you're making millions. Big brother gonna big brother", 106 + "This thread has me in TEARS. Y'all are hilarious 😭", 107 + "The fact that NBA refs had to break up a family dispute is sending me", 108 + "Both of them are gonna act like nothing happened next family reunion", 109 + "I guarantee their teammates are ROASTING them in the group chats right now", 110 + "This is the most relatable NBA drama I've ever seen. We all fought our siblings", 111 + "Mom's calling BOTH coaches after this I just know it", 112 + "The league office trying to figure out how to fine siblings for fighting each other", 113 + "This is gonna be an amazing 30 for 30 one day: 'What if I told you family and basketball don't always mix'", 114 + } 115 + 116 + func generateTID() string { 117 + // Simple TID generator for testing (timestamp in microseconds + random) 118 + now := time.Now().UnixMicro() 119 + return fmt.Sprintf("%d%04d", now, rand.Intn(10000)) 120 + } 121 + 122 + func createUser(db *sql.DB, handle, name string, idx int) (*User, error) { 123 + did := fmt.Sprintf("did:plc:testuser%d%d", time.Now().Unix(), idx) 124 + user := &User{ 125 + DID: did, 126 + Handle: handle, 127 + Name: name, 128 + } 129 + 130 + query := ` 131 + INSERT INTO users (did, handle, pds_url, created_at, updated_at) 132 + VALUES ($1, $2, $3, NOW(), NOW()) 133 + ON CONFLICT (did) DO NOTHING 134 + ` 135 + 136 + _, err := db.Exec(query, user.DID, user.Handle, "http://localhost:3001") 137 + if err != nil { 138 + return nil, fmt.Errorf("failed to create user: %w", err) 139 + } 140 + 141 + log.Printf("Created user: %s (%s)", user.Handle, user.DID) 142 + return user, nil 143 + } 144 + 145 + func createComment(db *sql.DB, user *User, content, parentURI, parentCID string, createdAt time.Time) (*Comment, error) { 146 + rkey := generateTID() 147 + uri := fmt.Sprintf("at://%s/social.coves.feed.comment/%s", user.DID, rkey) 148 + cid := fmt.Sprintf("bafy%s", rkey) 149 + 150 + comment := &Comment{ 151 + URI: uri, 152 + CID: cid, 153 + RKey: rkey, 154 + DID: user.DID, 155 + RootURI: postURI, 156 + RootCID: postCID, 157 + ParentURI: parentURI, 158 + ParentCID: parentCID, 159 + Content: content, 160 + CreatedAt: createdAt, 161 + } 162 + 163 + query := ` 164 + INSERT INTO comments ( 165 + uri, cid, rkey, commenter_did, root_uri, root_cid, 166 + parent_uri, parent_cid, content, created_at, indexed_at 167 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) 168 + ON CONFLICT (uri) DO NOTHING 169 + RETURNING id 170 + ` 171 + 172 + var id int64 173 + err := db.QueryRow(query, 174 + comment.URI, comment.CID, comment.RKey, comment.DID, 175 + comment.RootURI, comment.RootCID, comment.ParentURI, comment.ParentCID, 176 + comment.Content, comment.CreatedAt, 177 + ).Scan(&id) 178 + if err != nil { 179 + return nil, fmt.Errorf("failed to create comment: %w", err) 180 + } 181 + 182 + log.Printf("Created comment by %s: %.50s...", user.Handle, content) 183 + return comment, nil 184 + } 185 + 186 + func updateCommentCount(db *sql.DB, parentURI string, isPost bool) error { 187 + if isPost { 188 + _, err := db.Exec(` 189 + UPDATE posts 190 + SET comment_count = comment_count + 1 191 + WHERE uri = $1 192 + `, parentURI) 193 + return err 194 + } 195 + 196 + _, err := db.Exec(` 197 + UPDATE comments 198 + SET reply_count = reply_count + 1 199 + WHERE uri = $1 200 + `, parentURI) 201 + return err 202 + } 203 + 204 + func main() { 205 + // Connect to dev database 206 + dbURL := "postgres://dev_user:dev_password@localhost:5435/coves_dev?sslmode=disable" 207 + db, err := sql.Open("postgres", dbURL) 208 + if err != nil { 209 + log.Fatalf("Failed to connect to database: %v", err) 210 + } 211 + defer db.Close() 212 + 213 + if err := db.Ping(); err != nil { 214 + log.Fatalf("Failed to ping database: %v", err) 215 + } 216 + 217 + log.Println("Connected to database successfully!") 218 + log.Printf("Post URI: %s", postURI) 219 + log.Println("Starting to generate NBA test comments...") 220 + 221 + rand.Seed(time.Now().UnixNano()) 222 + 223 + // Create users 224 + log.Println("\n=== Creating Users ===") 225 + users := make([]*User, 0, len(userNames)) 226 + for i, name := range userNames { 227 + handle := fmt.Sprintf("%s.bsky.social", name) 228 + user, err := createUser(db, handle, name, i) 229 + if err != nil { 230 + log.Printf("Warning: Failed to create user %s: %v", name, err) 231 + continue 232 + } 233 + users = append(users, user) 234 + } 235 + 236 + log.Printf("\nCreated %d users", len(users)) 237 + 238 + // Generate comments with varied timing 239 + log.Println("\n=== Creating Top-Level Comments ===") 240 + baseTime := time.Now().Add(-3 * time.Hour) // Comments from 3 hours ago 241 + topLevelCommentsCreated := make([]*Comment, 0) 242 + 243 + // Create 18-22 top-level comments 244 + numTopLevel := 18 + rand.Intn(5) 245 + for i := 0; i < numTopLevel && i < len(users) && i < len(topLevelComments); i++ { 246 + user := users[i] 247 + content := topLevelComments[i] 248 + createdAt := baseTime.Add(time.Duration(i*4+rand.Intn(3)) * time.Minute) 249 + 250 + comment, err := createComment(db, user, content, postURI, postCID, createdAt) 251 + if err != nil { 252 + log.Printf("Warning: Failed to create top-level comment: %v", err) 253 + continue 254 + } 255 + 256 + topLevelCommentsCreated = append(topLevelCommentsCreated, comment) 257 + 258 + // Update post comment count 259 + if err := updateCommentCount(db, postURI, true); err != nil { 260 + log.Printf("Warning: Failed to update post comment count: %v", err) 261 + } 262 + 263 + // Small delay to avoid timestamp collisions 264 + time.Sleep(10 * time.Millisecond) 265 + } 266 + 267 + log.Printf("Created %d top-level comments", len(topLevelCommentsCreated)) 268 + 269 + // Create first-level replies (replies to top-level comments) 270 + log.Println("\n=== Creating First-Level Replies ===") 271 + firstLevelReplies := make([]*Comment, 0) 272 + 273 + for i, parentComment := range topLevelCommentsCreated { 274 + // 70% chance of having replies (NBA threads get lots of engagement) 275 + if rand.Float64() > 0.7 { 276 + continue 277 + } 278 + 279 + // 1-4 replies per comment 280 + numReplies := 1 + rand.Intn(4) 281 + for j := 0; j < numReplies && len(replyComments) > 0; j++ { 282 + userIdx := (i*3 + j + len(topLevelCommentsCreated)) % len(users) 283 + user := users[userIdx] 284 + content := replyComments[rand.Intn(len(replyComments))] 285 + createdAt := parentComment.CreatedAt.Add(time.Duration(3+rand.Intn(8)) * time.Minute) 286 + 287 + comment, err := createComment(db, user, content, parentComment.URI, parentComment.CID, createdAt) 288 + if err != nil { 289 + log.Printf("Warning: Failed to create first-level reply: %v", err) 290 + continue 291 + } 292 + 293 + firstLevelReplies = append(firstLevelReplies, comment) 294 + 295 + // Update parent comment reply count 296 + if err := updateCommentCount(db, parentComment.URI, false); err != nil { 297 + log.Printf("Warning: Failed to update comment reply count: %v", err) 298 + } 299 + 300 + time.Sleep(10 * time.Millisecond) 301 + } 302 + } 303 + 304 + log.Printf("Created %d first-level replies", len(firstLevelReplies)) 305 + 306 + // Create second-level replies (replies to replies) - deep threads 307 + log.Println("\n=== Creating Second-Level Replies ===") 308 + secondLevelCount := 0 309 + 310 + for i, parentComment := range firstLevelReplies { 311 + // 50% chance of having deep replies (NBA drama threads go DEEP) 312 + if rand.Float64() > 0.5 { 313 + continue 314 + } 315 + 316 + // 1-2 deep replies 317 + numReplies := 1 + rand.Intn(2) 318 + for j := 0; j < numReplies && len(deepReplyComments) > 0; j++ { 319 + userIdx := (i*2 + j + len(topLevelCommentsCreated) + len(firstLevelReplies)) % len(users) 320 + user := users[userIdx] 321 + content := deepReplyComments[rand.Intn(len(deepReplyComments))] 322 + createdAt := parentComment.CreatedAt.Add(time.Duration(2+rand.Intn(5)) * time.Minute) 323 + 324 + _, err := createComment(db, user, content, parentComment.URI, parentComment.CID, createdAt) 325 + if err != nil { 326 + log.Printf("Warning: Failed to create second-level reply: %v", err) 327 + continue 328 + } 329 + 330 + secondLevelCount++ 331 + 332 + // Update parent comment reply count 333 + if err := updateCommentCount(db, parentComment.URI, false); err != nil { 334 + log.Printf("Warning: Failed to update comment reply count: %v", err) 335 + } 336 + 337 + time.Sleep(10 * time.Millisecond) 338 + } 339 + } 340 + 341 + log.Printf("Created %d second-level replies", secondLevelCount) 342 + 343 + // Print summary 344 + totalComments := len(topLevelCommentsCreated) + len(firstLevelReplies) + secondLevelCount 345 + log.Println("\n=== Summary ===") 346 + log.Printf("Total users created: %d", len(users)) 347 + log.Printf("Total comments created: %d", totalComments) 348 + log.Printf(" - Top-level comments: %d", len(topLevelCommentsCreated)) 349 + log.Printf(" - First-level replies: %d", len(firstLevelReplies)) 350 + log.Printf(" - Second-level replies: %d", secondLevelCount) 351 + log.Println("\nDone! Check the NBACentral post for the brothers drama comments.") 352 + }