close

DEV Community

Cover image for I'm Not on the DEV Team. I Still Debugged Their Missing Badges From Public Code. (And My First Test Was Wrong.)
Dhruv Jani
Dhruv Jani Subscriber

Posted on AI-assisted

I'm Not on the DEV Team. I Still Debugged Their Missing Badges From Public Code. (And My First Test Was Wrong.)

A humbling reminder that passing tests can lie

Iโ€™m not on the DEV team. I donโ€™t have an admin panel, production database access, or internal Slack. Everything I know about how DEV works comes from reading the public forem/forem repository. ๐Ÿ•ต๏ธโ€โ™‚๏ธ

Today, that was enough to diagnose why nobody on DEV was receiving multi-commit contributor badges, ship the service fix upstream, and get a "Thank you for this" and merge from @benhalpern Sir himself. ๐Ÿš€

4x DEV Contributor Club badge
The 4x DEV Contributor Club badge: "Awarded for 4+ merged commits to Forem โ€” Thank you so much for your contributions!"

It also taught me a humbling lesson about tests that pass for the wrong reason. ๐Ÿงช


1. The Itch: 4 Merged PRs, 1 Badge ๐Ÿค”

Over the past few days, I went on a contribution sprint in forem/forem and merged four pull requests:

  • #24003 โ€” Docs: fix broken Kamal TOC anchor and index missing guides
  • #24056 โ€” A11y: remove duplicate aria-describedby attributes in admin forms
  • #24094 โ€” A11y: remove dangling aria-describedby references in creator settings
  • #24095 โ€” Views/Liquids: quote src and add missing alt in user_subscription tag

My 1x (dev-contributor) badge showed up right away after PR #1. โœ…

When PR #4 merged, I knew from config/schedule.yml that BadgeAchievements::BadgeAwardWorker runs every hour at :20 past the hour (cron: "20 * * * *"). I checked GitHubโ€™s Contributors API (/repos/forem/forem/contributors) and confirmed my account was sitting on page 2 with contributions: 4. Because Forem sets Octokit.auto_paginate = true, the worker was definitely reaching page 2.

One hourly cron passed. Then two. Then three. Still no 4x-commit-club badge. ๐Ÿ’€

Mr. Bean waiting and checking his watch
Me at :20 past the hour, every hour, refreshing my profile like it owes me money.

Instead of refreshing my profile for another 24 hours, I cracked open app/services/badges/award_contributor_from_github.rb. ๐Ÿ”


2. Reading the Code Like a Detective ๐Ÿ•ต๏ธโ€โ™‚๏ธ

Here is how Badges::AwardContributorFromGithub looked on main:

BADGE_SLUGS = {
  "dev-contributor": 1,
  "4x-commit-club": 4,
  "8x-commit-club": 8,
  "16x-commit-club": 16,
  "32x-commit-club": 32
}.freeze
Enter fullscreen mode Exit fullscreen mode

When I compared award_single_commit_contributors (which awarded my 1x badge) against award_multi_commit_contributors (which handles 4x, 8x, 16x, and 32x), three bugs jumped right off the screen:

๐Ÿ› Problem 1: Silent Slug Drops

Each key in BADGE_SLUGS is resolved via Badge.id_for_slug(slug). If a slug in the production badges table doesn't match the hardcoded constant, id_for_slug returns nil. The service called .compact on the resulting hash and moved on. No error. No log line. Just a badge tier that quietly vanishes into the void. ๐Ÿ‘ป

Because Forem only exposes resources :badges, only: [:index] without public /badges/:slug show routes, you can't query individual badge slugs directly. However, inspecting the public dev.to/badges catalog gave me a massive clue:

Badge #423 on /badges:

  • Title: 4x DEV Contributor Club
  • Description: 4x-commit-club

Waitโ€”why was 4x-commit-club sitting in the description field while the title was 4x DEV Contributor Club? Because Badge#generate_slug parameterizes the title when no explicit slug is set (4x-dev-contributor-club), the stored slug column in production didn't match the hardcoded "4x-commit-club" constant at all! ๐Ÿ’ก

Charlie Day conspiracy board
Connecting Badge#generate_slug to DOM element IDs on /badges at 9 AM with zero database access.

๐Ÿ’Œ Problem 2: Missing Context Messages

Single-commit badges were awarded with:

BadgeAchievement
  .where(user_id: i.user_id, badge_id: dev_contributor_badge_id)
  .first_or_create(rewarding_context_message_markdown: msg)
Enter fullscreen mode Exit fullscreen mode

Multi-commit badges, however, were awarded with a bare .create:

BadgeAchievement.create(user_id: user_id, badge_id: slug_id)
Enter fullscreen mode Exit fullscreen mode

Even when a multi-commit badge was created, it omitted rewarding_context_message_markdown: msg ("Thank you so much for your contributions!"). Four times the commits, zero times the thank-you note. ๐Ÿ˜ญ

๐Ÿ” Problem 3: Missing Idempotency Guard

create_badges_for_user called BadgeAchievement.create(...) on every hourly run instead of .where(...).first_or_create(...). Every hour, it re-attempted already-earned badges for every contributor on the list, relying on ActiveRecord's uniqueness validation to fail silently. Worse, if a badge ever had allow_multiple_awards: true, bare .create would insert a duplicate achievement every hourโ€”firing after_create_commit :notify_recipient and send_email_notification (flooding your inbox from yo@dev.to) each time. ๐Ÿ“ฌ๐Ÿ’ฅ


3. Filing the Issue & Shipping PR #24113 ๐Ÿ› ๏ธ

I opened Issue #24114 laying out the slug mismatch on badge IDs 423โ€“426, and paired it with PR #24113 to fix the service itself:

  1. โœ… Attached rewarding_context_message_markdown: msg to multi-commit badge awards.
  2. ๐Ÿ”’ Replaced bare BadgeAchievement.create with .where(...).first_or_create(...).
  3. ๐Ÿ“Š Deferred slug lookup behind the GitHub auth provider check and added logging whenever configured slugs are missing from the database.

4. The Plot Twist in Code Review ๐ŸŽฌ

The initial deep-dive review on @benhalpern Sirโ€™s account was posted by Forem's automated review agent (OpenHands), and it did something awesomeโ€”it ran a read-only lookup against the badges table and confirmed my exact diagnosis from Issue #24114:

"I ran a read-only lookup on the badges table for the five BADGE_SLUGS keys. Only one of the five slugs resolves. The four Nx-commit-club slugs have no matching row, so create_badges_for_user skips them and nobody can earn them today. The issue reporter's guess is correct."

Bingo. ๐ŸŽฏ

Even better, the review caught two subtle flaws in my PR that taught me more than the bug itself:

๐Ÿ“ข Lesson A: My Rails.logger.warn Was Log Spam

I originally logged a Rails.logger.warn inside the loop for each missing slug. Why was that bad?

  1. Until the production slugs were aligned, DEV would log 4 warn lines every hour.
  2. Forem powers many independent communities. Most self-hosted Forems enable GitHub auth without seeding DEV-specific contributor badges. Five warn lines an hour for expected configuration turns warn into permanent background noise.

The Fix (5e36999): Collect all missing slugs and emit a single Rails.logger.info line per run. ๐Ÿงน

๐Ÿคก Lesson B: My Regression Test Passed on main (For the Wrong Reason!)

To test idempotency, I wrote a spec that ran described_class.call twice and asserted that the user still had only 1 badge achievement for 4x-commit-club.

It passed! Green dots everywhere. ๐ŸŸข

Except the review pointed out something brutal: that test also passed on origin/main without my fix.

Surprised Pikachu
When your shiny new regression test passes on main without your bugfix.

Why? The badge factory defaults to allow_multiple_awards: false. On main, the second BadgeAchievement.create hit validates :badge_id, uniqueness: { scope: :user_id, if: :single_award_badge? }, failed validation silently, and left the count at 1. My test wasn't testing first_or_create at allโ€”it was testing ActiveRecord's uniqueness validator! ๐Ÿคฆโ€โ™‚๏ธ

The Fix (5e36999): I updated the spec to create the badge with allow_multiple_awards: true (where bare .create genuinely inserts duplicates on main) and asserted expect { described_class.call }.not_to change(BadgeAchievement, :count) on the second run.

After I pushed 5e36999 and all 34 checks went green, @benhalpern Sir stepped in personally, dropped a comment:

Ben Halpern approving PR #24113 with "Thank you for this"

That "Thank you for this" from @benhalpern Sir genuinely made my entire day! ๐Ÿ™Œ And right after that, he merged #24113 into forem:main as d1b962e (my 5th merged PR!). ๐ŸŽ‰

5 merged pull requests in forem/forem

Shortly after, the badge email from yo@dev.to hit my inbox and the 4x DEV Contributor Club badge landed on my profileโ€”complete with the "Thank you so much for your contributions!" context message my PR wired up. The code is cleaner, the PR is merged, and the green 4x circuit board is finally home. ๐Ÿ’š


๐Ÿ’ก 3 Takeaways Iโ€™d Tell Past Me

  1. ๐Ÿ”ด A regression test isn't real until you watch it fail on main. Before you celebrate a green spec, git stash your change and run the spec against main. If it still passes, you haven't written a regression testโ€”you've written a test that agrees with you.
  2. ๐Ÿชต Log levels are an operator contract. warn means someone should act. If a platform supports optional data seeds, missing optional rows are info, not warn.
  3. ๐Ÿ”“ You don't need production access to debug production. You just need the public source code, a falsifiable hypothesis, and the curiosity to follow the trail.

Your turn: Whatโ€™s the sneakiest time one of your tests passed for the completely wrong reason? Drop it in the comments ๐Ÿ‘‡


Top comments (65)

Collapse
 
ben profile image
Ben Halpern •

Thank you for this!

This bug has been around so long it had completely faded from everyone's consciounseness.

Picked as gem
Collapse
 
dj29 profile image
Dhruv Jani •

Thank you so much, Sir! That really means a lot. It was such an awesome rabbit hole to trace from the outside.
Also, your reply really made my day!
Always happy to support the DEV Community!

Have A Great Day!

Collapse
 
hanzla profile image
Hanzla Baig •

feat: Add post pinning support for organizations #24058

What type of PR is this? (check all applicable)

  • [ ] Refactor
  • [x] Feature
  • [ ] Bug Fix
  • [ ] Optimization
  • [ ] Documentation Update

Description

Extends the existing ProfilePin model (previously User-only) to also support Organization as a profile_type, reusing the polymorphic pin/unpin pattern already used on individual user profiles.

  • ProfilePin now validates Organization as a valid profile_type and checks that a pinned article belongs to the organization (via organization_id) when the profile is an Organization.
  • New OrganizationProfilePinsController (slug-scoped, reuses OrganizationAdminScoped for authorization) handles create/destroy of organization pins. Only org admins (or site admins) can pin/unpin.
  • stories_controller#handle_organization_index now loads the organization's pinned articles separately from the regular chronological feed and excludes them from it, mirroring assign_user_stories for users.
  • organizations/_main_feed renders a dedicated "Pinned" section above the regular feed when pinned posts exist, reusing the same badge/styling as the user profile pinned section.
  • Added a "Pin to Organization" section to the post management page, visible to org admins managing an org-owned post, to pin/unpin from there.
  • Up to 5 pins per organization (same cap as user profiles); reordering is not implemented, matching parity with the existing user-profile pin feature which also orders purely by published_at.

Related Tickets & Documents

  • Related Issue #24057
  • Closes #24057

QA Instructions, Screenshots, Recordings

  1. As an organization admin, go to /dashboard, open "Manage" on a post that belongs to your organization.
  2. Under the new "Pin to Organization" section, click "Pin to Organization".
  3. Visit the organization's page (/<org-slug>) โ€” the post now appears in a "Pinned" card above the regular feed, and does not also appear in the regular feed below.
  4. Pin up to 5 posts; a 6th pin attempt is rejected.
  5. Unpin from either the "Manage" page; the post returns to the regular chronological feed.
  6. A non-admin org member or an unaffiliated user cannot pin/unpin via POST /:slug/profile_pins (raises Pundit::NotAuthorizedError).

UI accessibility checklist

  • [x] Semantic HTML implemented? (reuses existing _main_feed/single_story markup and existing pin badge markup)
  • [x] Keyboard operability supported? (plain <button type="submit"> form controls)
  • [ ] Checked with axe DevTools and addressed Critical and Serious issues?
  • [x] Color contrast tested?

Added/updated tests?

  • [x] Yes
    • spec/models/profile_pin_spec.rb: organization pin validity, ownership check, 5-pin cap.
    • spec/requests/organization_profile_pins_spec.rb: create/destroy as org admin, five-pin cap, rejecting pins for articles outside the org, authorization failures for member/outsider/signed-out user.
    • spec/requests/stories_index_spec.rb: organization page renders the pinned section and does not duplicate the pinned post in the regular feed.

[optional] Are there any post deployment tasks we need to perform?

None.

Collapse
 
luke_bennett_ed52405c798e profile image
Luke Bennett •

Ok thanks

Collapse
 
dj29 profile image
Dhruv Jani •

๐Ÿ› Plot twist: the bug wasn't the only thing I had to debug. My own test was wrong, too.

This started with four merged PRs and a missing badge. It turned into a dive through Forem's public code, a fix that got merged, and a code review that taught me why a passing test isn't always proof that your fix works.

Huge thanks to everyone who reviewed the PR and helped me catch what I'd missed!

Your turn: Have you ever had a test pass green while completely missing the bug it was supposed to catch? I'd love to hear that story. ๐Ÿ‘€

Collapse
 
dj29 profile image
Dhruv Jani •

And Sir @francistrdev, I'd love a comment in here because I know you've contributed to forem before and skilled in ruby on rails.

Don't want to bother you, only when you're free!๐Ÿ˜…

Collapse
 
francistrdev profile image
FrancisTRแด…แด‡แด  •

I was wondering about this issue for a while. Thanks for fixing it!

@ben I know a lot of my PR's have been merged onto Forem and not received a badge for it, is it possible to fix that?

Thread Thread
 
francistrdev profile image
FrancisTRแด…แด‡แด  •

@dj29 Got it fixed lol

Turns out there is a use case where if you change your primary email, Forem will not detect your commits. I made it where it counts your merged prs instead of your commits. Makes it much simpler and handles that unique case.

I have another open PR to refine it, since initially I thought it didn't work. Since it works, as of today, not sure if that will be closed or merge. We will see haha

Thread Thread
 
dj29 profile image
Dhruv Jani •

That's great Sir @francistrdev, I actually saw your issue and PR both.๐Ÿ˜…

I'm kind of taking OSS contributions on forem way too seriously!๐Ÿ˜„

1 thing though, when you commit your changes to branch, the code rabbit and open hands review triggers by itself. I maybe wrong but I didn't have to call it by asking for full review.

Again really great!
Have A Great Day!

Thread Thread
 
francistrdev profile image
FrancisTRแด…แด‡แด  •

I think the Open Hand review @ben would have to do it manually (triggering the flow to do so). Code Rabbit is given for every PR regardless lol.

You can do commands for Code Rabbit, which is nice. I notice everytime I fix a bug or a suggestion, they keep on recommending it.

Thread Thread
 
bendevrelaye2e profile image
Ben Testing • • Edited

Just to confirm: Code Rabbit is tied to the repo and triggers automatically based on actions.

Open Hands is more of an experimental sidecar which I trigger and have some other algorithmic triggers, but isn't bound to repo i/o as nicely as Code Rabbit, so it's kind of random at this point how it dips in and out, but I'm refining it.

Ultimately myself or Mikey or sometimes another person in the company needs to stop by for a proper final review before anything gets merged.

(Btw Francis I got your email and considering the options, really appreciate your interest in helping more)

(Oops I posted this from my testing account ๐Ÿ˜„)

Thread Thread
 
ben profile image
Ben Halpern •

โ˜๏ธ I concur

Thread Thread
 
dj29 profile image
Dhruv Jani •

@ben Sir, can someone please solve this issue?

This is there since nearly 2 months and I've done countless mails and no action has been taken.

Also, somehow the MLH account now points to @star_dev

And I can't remove it! I lost 2 virtual stickers on hacktoberfest cause the DEV profile is wrongfully pointing to secondary mail.

Thread Thread
 
francistrdev profile image
FrancisTRแด…แด‡แด  • • Edited

Thread Thread
 
francistrdev profile image
FrancisTRแด…แด‡แด  •

Thanks Ben and Ben Variant :)

Thread Thread
 
francistrdev profile image
FrancisTRแด…แด‡แด  •

@ben @heyitsjem Can you resolve the issue? Still an issue to this day.

Collapse
 
annavi11arrea1 profile image
Anna Villarreal •

Thats so awesome! Good job!

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks! Have A Great Day!

Collapse
 
nyaomaru profile image
nyaomaru •

Great work! Nice contribution ๐Ÿ‘

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks!๐Ÿ˜„
Have A Great Day!

Collapse
 
nullandvoid_ profile image
Jyanthi •

The most valuable lesson here is that green tests donโ€™t always mean correct code! ๐Ÿ˜… Testing against the original code before celebrating a fix is such a simple but powerful habit. Loved how you turned a missing badge into a real open-source contribution! ๐Ÿ‘

Collapse
 
jstok5 profile image
John Stone •

Really enjoyed your perspective! It's great connecting with people who care about writing reliable code. Hope we can keep in touch and share ideas along the way!

Collapse
 
jstok5 profile image
John Stone •

Absolutely! Passing tests doesn't always guarantee correct code. Great insight! ๐Ÿ‘ I'd love to stay connected, exchange ideas, and keep in touch as friends!

Collapse
 
thomasbnt profile image
Thomas Bonnet •

Congrats!!

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks Sir!
Have A Great Day! and sorry for late comment!๐Ÿ˜…

Collapse
 
thomasbnt profile image
Thomas Bonnet •

thank you and good day!

Collapse
 
kaj_ilands profile image
Kaj •

Same class, from the other side. I published a fix for a 422 "Invalid authenticity token" on a scripted comment POST, then tested it by posting a comment. Green. Posted a second one to be sure. Green.

Then I ran the same recipe cold, fresh cookie jar, and it 422'd. The token I'd been grabbing off the article page belonged to a different form. It only ever worked because of state left over in a warm session.

The test wasn't wrong about the fix. It was wrong about the world. Same shape as your factory default: the thing that made it green was a default I didn't know I'd chosen.

Borrowed from your takeaway: for anything auth-shaped, "it passed" usually means "it passed with the session I happened to be holding." The regression run has to start from nobody. That's the git stash step, just for cookies.

(Token-side writeup, if the details are useful: dev.to/kaj_ilands/your-scripted-po...)

Collapse
 
mohsen_jr profile image
Mohsen jr •

The allow_multiple_awards: true detail is what makes the regression test meaningful. It removes the uniqueness validation as a safety net, so the test can actually expose duplicate creation on a second run. Good example of testing the failure mode, not just the expected outcome

Collapse
 
stackmasteraliza profile image
Aliza Ali •

Problem 1 is the one that sticks with me: a hardcoded constant quietly depending on a slug the database generated from a title. A cheap guard for DEV's own setup would be a spec or health check asserting that every key in BADGE_SLUGS resolves through Badge.id_for_slug. Then a renamed badge title fails CI loudly instead of switching off a whole badge tier for who knows how long. Debugging production from the public repo alone, with no DB access, is a seriously good skill to show off.

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks!
Have A Great Day! and sorry for late comment!๐Ÿ˜…

Collapse
 
parsa_m profile image
Parsa Mohammadi •

that moment when you realize the test passes because it hits a fixture instead of the pagination edge case is a classic. green CI gives you so much false confidence until the cron actually runs against the real list and quietly drops the tail.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.