我不在 DEV 團隊。我沒有管理後台、正式環境資料庫權限,也沒有內部 Slack。關於 DEV 的運作方式,我知道的一切都來自閱讀公開的 forem/forem 儲存庫。🕵️‍♂️

今天,光靠這些就足夠診斷出為什麼 DEV 上沒有人收到多次提交的貢獻者徽章、把服務修正往上游送出,還拿到 @benhalpern 本人回覆的 「Thank you for this」 和合併。🚀

4x DEV Contributor Club badge
4x DEV Contributor Club 徽章:「因為對 Forem 合併了 4 次以上提交,獲得此徽章——非常感謝你的貢獻!」

這也讓我學到了一個謙卑的教訓:有些測試雖然通過了,卻是因為錯誤的原因。🧪


1. 癢點:4 個合併的 PR,1 個徽章 🤔

過去幾天,我在 forem/forem 來了一波貢獻衝刺,合併了四個 pull request:

  • #24003 — 文件:修正壞掉的 Kamal TOC anchor 與缺少指南索引
  • #24056 — 無障礙:移除 admin 表單中重複的 aria-describedby 屬性
  • #24094 — 無障礙:移除 creator 設定中懸空的 aria-describedby 參照
  • #24095 — Views/Liquid:替 user_subscription tag 的 src 加上引號,並補上缺少的 alt

我的 1x(dev-contributor)徽章在 PR #1 合併後就立刻出現了。✅

當 PR #4 合併時,我根據 config/schedule.yml 知道 BadgeAchievements::BadgeAwardWorker 會在每小時的 :20 執行(cron: "20 * * * *")。我查了 GitHub 的 Contributors API(/repos/forem/forem/contributors),確認我的帳號在第 2 頁,contributions: 4。由於 Forem 設定了 Octokit.auto_paginate = true,worker 肯定有抓到第 2 頁。

等了一個整點 cron。又等兩個。再等三個。還是沒有 4x-commit-club 徽章。💀

Mr. Bean waiting and checking his watch
每個小時的 :20,我都像這樣刷新個人檔案,彷彿它欠我錢。

與其再刷新個人檔案 24 小時,我乾脆直接打開 app/services/badges/award_contributor_from_github.rb。🔍


2. 像偵探一樣讀程式碼 🕵️‍♂️

Badges::AwardContributorFromGithub 在 main 上長這樣:

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

當我把 award_single_commit_contributors(它有發給我的 1x 徽章)跟 award_multi_commit_contributors(負責 4x、8x、16x 和 32x)對比時,三個 bug 立刻跳進眼裡:

🐛 問題 1:slug 靜悄悄被丟掉

BADGE_SLUGS 裡的每個 key 都會經由 Badge.id_for_slug(slug) 解析。如果 production 的 badges 資料表裡某個 slug 跟硬編碼常數不一致,id_for_slug 就會回傳 nil。這個 service 接著對結果 hash 呼叫 .compact 然後繼續往下跑。沒有錯誤。沒有 log。只有某個徽章等級默默消失在虛空中。👻

因為 Forem 只公開 resources :badges, only: [:index],沒有 /badges/:slug 這種公開 show 路由,所以你沒辦法直接查單一徽章 slug。不過,查看公開的 dev.to/badges 型錄時,我立刻得到一個超大的線索:

/badges 上的第 423 個徽章:

  • 標題: 4x DEV Contributor Club
  • 描述: 4x-commit-club

等等——為什麼 4x-commit-club 會出現在 description 欄位,而標題卻是 4x DEV Contributor Club?因為當沒有明確設定 slug 時,Badge#generate_slug 會把標題做 parameterize(4x-dev-contributor-club),所以 production 裡儲存的 slug 欄位,根本就跟硬編碼的 "4x-commit-club" 常數對不上!💡

Charlie Day conspiracy board
早上 9 點,零資料庫權限,卻把 Badge#generate_slug 和 /badges 的 DOM element IDs 串起來。

💌 問題 2:缺少內容訊息

單次提交徽章是這樣發的:

BadgeAchievement
  .where(user_id: i.user_id, badge_id: dev_contributor_badge_id)
  .first_or_create(rewarding_context_message_markdown: msg)

但多次提交徽章卻只是直接 .create:

BadgeAchievement.create(user_id: user_id, badge_id: slug_id)

就算多次提交徽章真的建立成功了,它也漏掉了 rewarding_context_message_markdown: msg("Thank you so much for your contributions!")。提交了四次,卻一點感謝訊息都沒有。😭

🔁 問題 3:缺少冪等性保護

create_badges_for_user 每小時執行時,都直接呼叫 BadgeAchievement.create(...),而不是 .where(...).first_or_create(...)。每小時它都會替清單中的每位貢獻者重新嘗試建立已經拿過的徽章,仰賴 ActiveRecord 的 uniqueness 驗證 默默失敗。更糟的是,如果某個徽章曾經設定 allow_multiple_awards: true,那麼單純的 .create 會每小時都插入一筆重複的 achievement——每次都觸發 after_create_commit :notify_recipient 和 send_email_notification(從 [email protected] 把你的信箱灌爆)。📬💥


3. 提 issue 並送出 PR #24113 🛠️

我開了 Issue #24114,把 badge ID 423–426 的 slug 不匹配問題整理清楚,並搭配 PR #24113 來修正這個 service:

  1. ✅ 在多次提交徽章發放時加入 rewarding_context_message_markdown: msg。
  2. 🔒 把裸的 BadgeAchievement.create 改成 .where(...).first_or_create(...)。
  3. 📊 把 slug 查詢延後到 GitHub 驗證提供者檢查之後,並在配置的 slug 沒有出現在資料庫時加上 log。

4. Code Review 的劇情轉折 🎬

在 @benhalpern 本人帳號上的初次深入 review,是由 Forem 的自動化 review agent(OpenHands)發出的,而且它做了一件超讚的事——它對 badges 資料表做了唯讀查詢,並確認了我在 Issue #24114 裡的診斷完全正確:

「我對 badges 資料表中五個 BADGE_SLUGS key 做了唯讀查詢。只有其中一個 slug 能解析成功。那四個 Nx-commit-club slug 都沒有對應資料列,所以 create_badges_for_user 會略過它們,今天沒有人能拿到這些徽章。issue 回報者的推測是正確的。」

中了。🎯

更棒的是,review 還抓到了我 PR 裡另外兩個微妙的缺陷,讓我學到的東西甚至比原本的 bug 還多:

📢 教訓 A:我的 Rails.logger.warn 會造成 log 噪音

我一開始是在每個缺少的 slug 都各自記一條 Rails.logger.warn。為什麼這樣不好?

  1. 在 production slug 還沒對齊之前,DEV 每小時會多出 4 條 warn log。
  2. Forem 服務很多獨立社群。多數自架 Forem 會啟用 GitHub 驗證,但不會預先建立 DEV 專用的貢獻者徽章。對於預期內的設定差異,每小時 5 條 warn 只會變成永遠存在的背景噪音。

修正(5e36999): 收集所有缺少的 slug,然後每次執行只輸出一條 Rails.logger.info。🧹

🤡 教訓 B:我的迴歸測試在 main 上也通過了(但原因錯了!)

為了測試冪等性,我寫了一個 spec,連跑兩次 described_class.call,並斷言使用者對 4x-commit-club 仍然只有 1 筆 badge achievement。

它通過了!滿滿的綠點。🟢

但 review 指出了一個很殘酷的事實:這個測試在我沒有修正的 origin/main 上也會通過。

Surprised Pikachu
當你那個閃亮的新迴歸測試,在沒有 bugfix 的 main 上也通過時。

為什麼?因為 badge factory 預設 allow_multiple_awards: false。在 main 上,第二次 BadgeAchievement.create 會撞上 validates :badge_id, uniqueness: { scope: :user_id, if: :single_award_badge? },驗證默默失敗,數量維持在 1。我的測試根本沒有在測 first_or_create,而是在測 ActiveRecord 的 uniqueness validator!🤦‍♂️

修正(5e36999): 我把 spec 改成建立 allow_multiple_awards: true 的徽章(這樣在 main 上裸 .create 才會真的插入重複資料),並且在第二次執行時斷言 expect { described_class.call }.not_to change(BadgeAchievement, :count)。

當我 push 了 5e36999、34 項檢查全綠之後,@benhalpern 本人親自出手,留下了這則評論:

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

@benhalpern 本人的那句 「Thank you for this」 真的讓我一整天都值回票價!🙌 緊接著,他把 #24113 合併進 forem:main,成為 d1b962e(我第 5 個被合併的 PR!)。🎉

5 merged pull requests in forem/forem

不久之後,來自 [email protected] 的徽章信就進了我的信箱,4x DEV Contributor Club 徽章也出現在我的個人檔案上——連同我 PR 裡補上的 「Thank you so much for your contributions!」 內容訊息一起。程式碼更乾淨了,PR 也合併了,而那塊綠色的 4x 電路板,終於回家了。💚


💡 我會告訴過去自己的 3 個重點

  1. 🔴 迴歸測試只有在你看過它在 main 上失敗時,才算真的成立。 在你慶祝綠燈 spec 之前,先 git stash 你的變更,並在 main 上跑一次 spec。如果它還是過了,那你寫的就不是迴歸測試,而是一個只是在附和你的測試。
  2. 🪵 log level 是給維運者的契約。 warn 表示應該有人要採取行動。如果平台支援可選的資料 seed,那麼缺少可選資料列應該記 info,不是 warn。
  3. 🔓 你不需要 production 權限也能除錯 production。 你只需要公開原始碼、可被推翻的假設,以及追蹤線索的好奇心。

換你說: 你遇過最狡猾的一次測試,居然是因為完全錯誤的原因才通過的嗎?留言告訴我吧 👇


原文出處:https://dev.to/dj29/im-not-on-the-dev-team-i-still-debugged-their-missing-badges-from-public-code-and-my-first-test-18oo


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝9   💬1  
320
🥈
mike
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登