From 9c832621b718e730153cfce2ad3dc448b945d250 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 08:26:06 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E4=BE=9D=E9=A0=BC=E3=82=B9=E3=83=AC=E3=83=83=E3=83=89=E3=81=AE?= =?UTF-8?q?=E5=BE=8C=E7=B6=9A=E9=80=9A=E7=9F=A5=E3=81=AE=E6=96=87=E8=A8=80?= =?UTF-8?q?=E3=82=92=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=8B=EF=BC=88?= =?UTF-8?q?#104=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub の reason はスレッドの購読理由でありイベント種別ではないため、 一度レビュー依頼された PR は以降のコメントも review_requested で届き、 常に「レビューを依頼されました」と表示されていた。 latest_comment_url が subject.url と異なる(=コメントが起点)場合に限り FOLLOWUP_MESSAGES の文言へ差し替える。コメントがまだ無いスレッドでは latest_comment_url に subject.url と同じ値が入るため、依頼直後の通知は 従来どおりの文言のままになる。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MoGxJDiZa8c2E3PVAnxbK8 --- spec/github/models_spec.cr | 55 +++++++++++++++++++++++++++++++++++-- spec/github/usecase_spec.cr | 11 +++++++- src/github/models.cr | 29 +++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/spec/github/models_spec.cr b/spec/github/models_spec.cr index 6f2449e..e261dde 100644 --- a/spec/github/models_spec.cr +++ b/spec/github/models_spec.cr @@ -61,6 +61,30 @@ describe Github::Subject do end end + describe "#comment_triggered?" do + it "is true when latest_comment_url points to 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.comment_triggered?.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.comment_triggered?.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").comment_triggered?.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,33 @@ 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 when a review-requested thread is updated by 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 "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 +206,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..5b08575 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 a follow-up comment on a 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 15fc9ab..1e112ef 100644 --- a/src/github/models.cr +++ b/src/github/models.cr @@ -32,6 +32,18 @@ module Github "invitation" => "招待が届きました", } + # 「一度きりの出来事」を指す reason 向けの、2 回目以降の文言。 + # + # GitHub の reason は「そのスレッドを購読している理由」であってイベント種別 + # ではないため、一度レビュー依頼された PR は以降のコメントや更新もすべて + # review_requested で届く。REASON_MESSAGES だけだと常に「レビューを依頼され + # ました」になり通知理由が実態と合わないので、コメントが起点の通知に限り + # 文言を差し替える(issue #104)。ここに reason を足せば他の reason にも + # 同じ切り替えを適用できる。 + FOLLOWUP_MESSAGES = { + "review_requested" => "レビュー依頼中の PR にコメントがつきました", + } + GENERIC_MESSAGE = "なにかあったみたいです。確認してみましょう!" getter subject : Subject @@ -47,6 +59,11 @@ module Github end def reason_message : String + if subject.comment_triggered? + followup = FOLLOWUP_MESSAGES[reason]? + return followup if followup + end + REASON_MESSAGES[reason]? || GENERIC_MESSAGE end @@ -139,6 +156,18 @@ module Github type.in?(BODY_TYPES) ? url : "" end + # 通知の起点がコメントかどうか。latest_comment_url はスレッド最新コメントの + # URL だが、コメントがまだ無いスレッドでは subject.url と同じ値が入る。 + # よって url と異なる値のときだけ「コメントが起点」と判断できる(issue #104)。 + # + # 既にコメントの付いた PR に後からレビュー依頼された場合、起点は依頼でも + # 直前のコメント URL が入るため誤判定する。通知 payload にイベント種別は + # 無く、追加の API 呼び出し無しでは区別できないため許容する。 + def comment_triggered? : Bool + return false unless comment = latest_comment_url.presence + comment != url + end + # subject.url 末尾の PR / Issue / Discussion 番号。番号が意味を持つ type に # 限り、末尾セグメントが数値なら返す。Commit(末尾が SHA)や末尾スラッシュ、 # URL が無い場合などは nil を返す(issue #96)。 From d265ca42eaf86245cd8850a202119874a989f513 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 02:42:13 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E3=82=A2=E3=82=B5=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E6=B8=88=E3=81=BF=E3=82=B9=E3=83=AC=E3=83=83=E3=83=89=E3=81=AE?= =?UTF-8?q?=E5=BE=8C=E7=B6=9A=E9=80=9A=E7=9F=A5=E3=81=AE=E6=96=87=E8=A8=80?= =?UTF-8?q?=E3=82=82=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=8B=EF=BC=88?= =?UTF-8?q?#104=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review_requested と同じく、一度アサインされた PR/Issue は以降のコメントも reason=assign で届くため、常に「アサインされました」になっていた。 FOLLOWUP_MESSAGES に assign を追加して同じ切り替えを適用する。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MoGxJDiZa8c2E3PVAnxbK8 --- spec/github/models_spec.cr | 18 ++++++++++++++++++ src/github/models.cr | 11 ++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/spec/github/models_spec.cr b/spec/github/models_spec.cr index e1766e5..711e466 100644 --- a/spec/github/models_spec.cr +++ b/spec/github/models_spec.cr @@ -143,6 +143,24 @@ describe Github::Notification do notification.reason_message.should eq "レビュー依頼中の PR にコメントがつきました" end + it "switches to a follow-up message when an assigned thread is updated by 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", diff --git a/src/github/models.cr b/src/github/models.cr index 5af7b87..3317992 100644 --- a/src/github/models.cr +++ b/src/github/models.cr @@ -35,13 +35,14 @@ module Github # 「一度きりの出来事」を指す reason 向けの、2 回目以降の文言。 # # GitHub の reason は「そのスレッドを購読している理由」であってイベント種別 - # ではないため、一度レビュー依頼された PR は以降のコメントや更新もすべて - # review_requested で届く。REASON_MESSAGES だけだと常に「レビューを依頼され - # ました」になり通知理由が実態と合わないので、コメントが起点の通知に限り - # 文言を差し替える(issue #104)。ここに reason を足せば他の reason にも - # 同じ切り替えを適用できる。 + # ではないため、一度レビュー依頼/アサインされた PR・Issue は、以降のコメントや + # 更新もすべて同じ reason で届く。REASON_MESSAGES だけだと常に「レビューを依頼 + # されました」「アサインされました」になり通知理由が実態と合わないので、 + # コメントが起点の通知に限り文言を差し替える(issue #104)。 + # ここに reason を足せば他の reason にも同じ切り替えを適用できる。 FOLLOWUP_MESSAGES = { "review_requested" => "レビュー依頼中の PR にコメントがつきました", + "assign" => "担当している PR/Issue にコメントがつきました", } GENERIC_MESSAGE = "なにかあったみたいです。確認してみましょう!" From 8d9bcccdf28f0c0087bbaaba7adbb8f3e545d8dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 15:49:54 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E5=BE=8C=E7=B6=9A=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E3=81=AE=E6=96=87=E8=A8=80=E3=82=92=E3=82=B3=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=AB=E9=99=90=E5=AE=9A=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E8=A1=A8=E7=8F=BE=E3=81=AB=E3=81=99=E3=82=8B=EF=BC=88#104?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit latest_comment_url は通知を発生させたイベントではなくスレッドの現在の最新 コメントを指すため、コメント済みスレッドに push や状態変更が来た通知でも 真になる。「コメントがつきました」だと過去のコメントを新着と誤認させる。 - 文言を「〜に動きがありました」に変更し、何が起きたかを限定しない - 述語名を comment_triggered? -> commented? に変更。実際に判定しているのは 「通知の起点がコメントか」ではなく「スレッドにコメントが存在するか」 - コメントで上記の性質と限界を明記 PR #106 のレビュー指摘対応。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MoGxJDiZa8c2E3PVAnxbK8 --- spec/github/models_spec.cr | 18 +++++++++--------- spec/github/usecase_spec.cr | 4 ++-- src/github/models.cr | 29 ++++++++++++++++++----------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/spec/github/models_spec.cr b/spec/github/models_spec.cr index 711e466..ad916ca 100644 --- a/spec/github/models_spec.cr +++ b/spec/github/models_spec.cr @@ -61,14 +61,14 @@ describe Github::Subject do end end - describe "#comment_triggered?" do - it "is true when latest_comment_url points to a comment" do + 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.comment_triggered?.should be_true + subject.commented?.should be_true end it "is false when latest_comment_url mirrors the subject url (no comment yet)" do @@ -77,11 +77,11 @@ describe Github::Subject do url: "https://api.github.com/repos/o/r/pulls/1", latest_comment_url: "https://api.github.com/repos/o/r/pulls/1", ) - subject.comment_triggered?.should be_false + 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").comment_triggered?.should be_false + subject_from("PullRequest", url: "https://api.github.com/repos/o/r/pulls/1").commented?.should be_false end end @@ -134,22 +134,22 @@ describe Github::Notification do notification_from("some_future_reason").reason_message.should eq Github::Notification::GENERIC_MESSAGE end - it "switches to a follow-up message when a review-requested thread is updated by a comment" do + 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 にコメントがつきました" + notification.reason_message.should eq "レビュー依頼中の PR に動きがありました" end - it "switches to a follow-up message when an assigned thread is updated by a comment" do + 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 にコメントがつきました" + notification.reason_message.should eq "担当している PR/Issue に動きがありました" end it "keeps the assign message while the thread has no comment" do diff --git a/spec/github/usecase_spec.cr b/spec/github/usecase_spec.cr index 5b08575..796b8a3 100644 --- a/spec/github/usecase_spec.cr +++ b/spec/github/usecase_spec.cr @@ -41,12 +41,12 @@ describe Github::Usecase do build(notification(reason: "review_requested"), comment).pretext.should eq "[Issue] レビューを依頼されました" end - it "reflects a follow-up comment on a review-requested thread in the pretext" do + 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 にコメントがつきました" + build(notify, comment).pretext.should eq "[Issue] レビュー依頼中の PR に動きがありました" end it "formats the title as owner/repo#number title" do diff --git a/src/github/models.cr b/src/github/models.cr index 3317992..d6bd7ac 100644 --- a/src/github/models.cr +++ b/src/github/models.cr @@ -38,11 +38,14 @@ module Github # ではないため、一度レビュー依頼/アサインされた PR・Issue は、以降のコメントや # 更新もすべて同じ reason で届く。REASON_MESSAGES だけだと常に「レビューを依頼 # されました」「アサインされました」になり通知理由が実態と合わないので、 - # コメントが起点の通知に限り文言を差し替える(issue #104)。 + # 初回ではないと判断できる通知は文言を差し替える(issue #104)。 + # + # 何が起きたか(コメントか push か状態変更か)は通知 payload から判別できない + # ため、文言はコメントに限定せず「動きがありました」に留める。 # ここに reason を足せば他の reason にも同じ切り替えを適用できる。 FOLLOWUP_MESSAGES = { - "review_requested" => "レビュー依頼中の PR にコメントがつきました", - "assign" => "担当している PR/Issue にコメントがつきました", + "review_requested" => "レビュー依頼中の PR に動きがありました", + "assign" => "担当している PR/Issue に動きがありました", } GENERIC_MESSAGE = "なにかあったみたいです。確認してみましょう!" @@ -60,7 +63,7 @@ module Github end def reason_message : String - if subject.comment_triggered? + if subject.commented? followup = FOLLOWUP_MESSAGES[reason]? return followup if followup end @@ -157,14 +160,18 @@ module Github type.in?(BODY_TYPES) ? url : "" end - # 通知の起点がコメントかどうか。latest_comment_url はスレッド最新コメントの - # URL だが、コメントがまだ無いスレッドでは subject.url と同じ値が入る。 - # よって url と異なる値のときだけ「コメントが起点」と判断できる(issue #104)。 + # スレッドにコメントが 1 件以上付いているか。latest_comment_url はスレッドの + # 最新コメントの URL だが、コメントがまだ無いスレッドでは subject.url と同じ + # 値が入る。よって url と異なる値のときだけコメントありと判断できる。 # - # 既にコメントの付いた PR に後からレビュー依頼された場合、起点は依頼でも - # 直前のコメント URL が入るため誤判定する。通知 payload にイベント種別は - # 無く、追加の API 呼び出し無しでは区別できないため許容する。 - def comment_triggered? : Bool + # あくまで「コメントが存在するか」であって「今回の通知の起点がコメントか」では + # ない点に注意。latest_comment_url は通知を発生させたイベントではなくスレッドの + # 現在の最新コメントを指すため、コメント済みスレッドに push や状態変更が来た + # 通知でも真になる。通知 payload にイベント種別が無く、追加の API 呼び出し + # 無しでは区別できないので、これを「初回ではない=その後の動き」の目安として + # 使い、文言側はコメントに限定しない表現にしている(issue #104 / PR #106 + # レビュー指摘)。 + def commented? : Bool return false unless comment = latest_comment_url.presence comment != url end