diff --git a/spec/github/models_spec.cr b/spec/github/models_spec.cr index c11c451..ad916ca 100644 --- a/spec/github/models_spec.cr +++ b/spec/github/models_spec.cr @@ -61,6 +61,30 @@ describe Github::Subject do end end + describe "#commented?" do + it "is true when the thread has a comment" do + subject = subject_from( + "PullRequest", + url: "https://api.github.com/repos/o/r/pulls/1", + latest_comment_url: "https://api.github.com/repos/o/r/issues/comments/1", + ) + subject.commented?.should be_true + end + + it "is false when latest_comment_url mirrors the subject url (no comment yet)" do + subject = subject_from( + "PullRequest", + url: "https://api.github.com/repos/o/r/pulls/1", + latest_comment_url: "https://api.github.com/repos/o/r/pulls/1", + ) + subject.commented?.should be_false + end + + it "is false when latest_comment_url is blank" do + subject_from("PullRequest", url: "https://api.github.com/repos/o/r/pulls/1").commented?.should be_false + end + end + describe "#number" do it "extracts a trailing issue/PR number from the url" do subject_from("Issue", url: "https://api.github.com/repos/o/r/issues/42").number.should eq "42" @@ -109,6 +133,51 @@ describe Github::Notification do it "falls back to a generic message for unknown reasons" do notification_from("some_future_reason").reason_message.should eq Github::Notification::GENERIC_MESSAGE end + + it "switches to a follow-up message once a review-requested thread has a comment" do + notification = notification_from( + "review_requested", + url: "https://api.github.com/repos/o/r/pulls/1", + latest_comment_url: "https://api.github.com/repos/o/r/issues/comments/1", + ) + notification.reason_message.should eq "レビュー依頼中の PR に動きがありました" + end + + it "switches to a follow-up message once an assigned thread has a comment" do + notification = notification_from( + "assign", + url: "https://api.github.com/repos/o/r/issues/1", + latest_comment_url: "https://api.github.com/repos/o/r/issues/comments/1", + ) + notification.reason_message.should eq "担当している PR/Issue に動きがありました" + end + + it "keeps the assign message while the thread has no comment" do + notification = notification_from( + "assign", + url: "https://api.github.com/repos/o/r/issues/1", + latest_comment_url: "https://api.github.com/repos/o/r/issues/1", + ) + notification.reason_message.should eq "アサインされました" + end + + it "keeps the review-requested message while the thread has no comment" do + notification = notification_from( + "review_requested", + url: "https://api.github.com/repos/o/r/pulls/1", + latest_comment_url: "https://api.github.com/repos/o/r/pulls/1", + ) + notification.reason_message.should eq "レビューを依頼されました" + end + + it "keeps the reason message for reasons without a follow-up variant" do + notification = notification_from( + "comment", + url: "https://api.github.com/repos/o/r/pulls/1", + latest_comment_url: "https://api.github.com/repos/o/r/issues/comments/1", + ) + notification.reason_message.should eq "コメントがつきました" + end end describe "#pretext" do @@ -155,10 +224,10 @@ describe Github::Notification do end end -private def notification_from(reason : String) +private def notification_from(reason : String, url = "", latest_comment_url = "") Github::Notification.from_json({ reason: reason, - subject: {type: "Issue", title: "title"}, + subject: {type: "Issue", title: "title", url: url, latest_comment_url: latest_comment_url}, repository: {owner: {login: "octocat"}}, updated_at: "2026-07-14T00:00:00Z", }.to_json) diff --git a/spec/github/usecase_spec.cr b/spec/github/usecase_spec.cr index 029214e..796b8a3 100644 --- a/spec/github/usecase_spec.cr +++ b/spec/github/usecase_spec.cr @@ -17,10 +17,11 @@ private def notification( url = "https://api.github.com/repos/octocat/Hello-World/issues/42", reason = "review_requested", repo_html_url : String? = "https://github.com/octocat/Hello-World", + latest_comment_url = "", ) Github::Notification.from_json({ reason: reason, - subject: {type: "Issue", title: "Spurious failure", url: url}, + subject: {type: "Issue", title: "Spurious failure", url: url, latest_comment_url: latest_comment_url}, repository: {full_name: "octocat/Hello-World", html_url: repo_html_url, owner: {login: "octocat"}}, updated_at: "2026-07-14T00:00:00Z", }.to_json) @@ -40,6 +41,14 @@ describe Github::Usecase do build(notification(reason: "review_requested"), comment).pretext.should eq "[Issue] レビューを依頼されました" end + it "reflects the follow-up wording for a commented review-requested thread in the pretext" do + notify = notification( + reason: "review_requested", + latest_comment_url: "https://api.github.com/repos/octocat/Hello-World/issues/comments/1", + ) + build(notify, comment).pretext.should eq "[Issue] レビュー依頼中の PR に動きがありました" + end + it "formats the title as owner/repo#number title" do build(notification, comment).title.should eq "octocat/Hello-World#42 Spurious failure" end diff --git a/src/github/models.cr b/src/github/models.cr index 7ad6cd0..d6bd7ac 100644 --- a/src/github/models.cr +++ b/src/github/models.cr @@ -32,6 +32,22 @@ module Github "invitation" => "招待が届きました", } + # 「一度きりの出来事」を指す reason 向けの、2 回目以降の文言。 + # + # GitHub の reason は「そのスレッドを購読している理由」であってイベント種別 + # ではないため、一度レビュー依頼/アサインされた PR・Issue は、以降のコメントや + # 更新もすべて同じ reason で届く。REASON_MESSAGES だけだと常に「レビューを依頼 + # されました」「アサインされました」になり通知理由が実態と合わないので、 + # 初回ではないと判断できる通知は文言を差し替える(issue #104)。 + # + # 何が起きたか(コメントか push か状態変更か)は通知 payload から判別できない + # ため、文言はコメントに限定せず「動きがありました」に留める。 + # ここに reason を足せば他の reason にも同じ切り替えを適用できる。 + FOLLOWUP_MESSAGES = { + "review_requested" => "レビュー依頼中の PR に動きがありました", + "assign" => "担当している PR/Issue に動きがありました", + } + GENERIC_MESSAGE = "なにかあったみたいです。確認してみましょう!" getter subject : Subject @@ -47,6 +63,11 @@ module Github end def reason_message : String + if subject.commented? + followup = FOLLOWUP_MESSAGES[reason]? + return followup if followup + end + REASON_MESSAGES[reason]? || GENERIC_MESSAGE end @@ -139,6 +160,22 @@ module Github type.in?(BODY_TYPES) ? url : "" end + # スレッドにコメントが 1 件以上付いているか。latest_comment_url はスレッドの + # 最新コメントの URL だが、コメントがまだ無いスレッドでは subject.url と同じ + # 値が入る。よって url と異なる値のときだけコメントありと判断できる。 + # + # あくまで「コメントが存在するか」であって「今回の通知の起点がコメントか」では + # ない点に注意。latest_comment_url は通知を発生させたイベントではなくスレッドの + # 現在の最新コメントを指すため、コメント済みスレッドに push や状態変更が来た + # 通知でも真になる。通知 payload にイベント種別が無く、追加の API 呼び出し + # 無しでは区別できないので、これを「初回ではない=その後の動き」の目安として + # 使い、文言側はコメントに限定しない表現にしている(issue #104 / PR #106 + # レビュー指摘)。 + def commented? : Bool + return false unless comment = latest_comment_url.presence + comment != url + end + # subject.url 末尾の PR / Issue / Discussion 番号。番号が意味を持つ type に # 限り、末尾セグメントが数値なら返す。Commit(末尾が SHA)や末尾スラッシュ、 # URL が無い場合などは nil を返す(issue #96)。