@@ -127,6 +127,31 @@ def test_cooccurring_hashtags(con, sources):
127127 ]
128128
129129
130+ def _tag_written_both_ways (con ) -> None :
131+ """The same hashtag as the two sides really store it: the rollup lowercases, while the base
132+ `changesets` table keeps the case the mapper typed."""
133+ con .execute ("INSERT INTO history SELECT '#youthmappers', * EXCLUDE (hashtag) FROM history WHERE changeset_id = 1" )
134+ con .execute ("UPDATE csets SET hashtags = list_append(hashtags, '#YouthMappers') WHERE changeset_id = 3" )
135+
136+
137+ def test_cooccurring_hashtags_merge_case_variants (con , sources ):
138+ # Grouping is on the raw string, so a tag written both ways used to come back as two rows, each
139+ # holding only part of the contributors. Both sides lowercase before grouping.
140+ _tag_written_both_ways (con )
141+ tags = [r ["hashtag" ] for r in query .hashtags (con , "hotosm" , sources )]
142+ assert "#YouthMappers" not in tags
143+ assert tags .count ("#youthmappers" ) == 1
144+
145+
146+ def test_leaderboard_cooccurring_hashtags_merge_case_variants (con , sources ):
147+ # Same split on the per-user chips: alice carries the tag from history and from the recent tail.
148+ _tag_written_both_ways (con )
149+ items = query .leaderboard (con , "hotosm" , sources )["items" ]
150+ alice = next (r for r in items if r ["name" ] == "alice" )
151+ assert "#YouthMappers" not in alice ["hashtags" ]
152+ assert alice ["hashtags" ].count ("#youthmappers" ) == 1
153+
154+
130155def test_tags_breakdown (con , sources ):
131156 tg = query .tags (con , "hotosm" , sources )
132157 building = next (r for r in tg if r ["tag_key" ] == "building" )
0 commit comments