From 13b7a942572a695cdba51093938c8915ca7bff68 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:08:56 +0200 Subject: [PATCH 1/3] forge_notify: fix notification type strings and PR state detection (fixes #201) --- hive-ag3nt/src/forge_notify.rs | 63 ++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 5f76608c..048b7180 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -158,8 +158,9 @@ async fn format_notification( .unwrap_or("") .trim(); + // Forgejo API returns "Pull" / "Issue", never "Pull Request". let kind = match notif_type { - "Pull Request" => "comment on PR", + "Pull" => "comment on PR", "Issue" => "comment on issue", _ => "comment", }; @@ -174,40 +175,42 @@ async fn format_notification( out } else { // Notification triggered by creation or state change of the subject. - let subject = fetch_json(client, subject_api_url, token).await; - let author = subject - .as_ref() - .and_then(|s| s["user"]["login"].as_str()) - .unwrap_or("?"); - let body = subject - .as_ref() - .and_then(|s| s["body"].as_str()) - .unwrap_or("") - .trim(); - let state = subject - .as_ref() - .and_then(|s| s["state"].as_str()) - .unwrap_or(""); - let merged = subject - .as_ref() - .and_then(|s| s["merged"].as_bool()) - .unwrap_or(false); + // + // Classification uses notif["subject"]["state"] directly — Forgejo + // returns "open" / "closed" / "merged" here. We do NOT rely on + // fetching the PR/issue detail for `merged`: + // - `subject.url` points to the *issues* endpoint, which returns + // `pull_request.merged`, not top-level `merged`. + // - Forgejo API type is "Pull" / "Issue", never "Pull Request". + let notif_state = notif["subject"]["state"].as_str().unwrap_or(""); - let kind = match (notif_type, state, merged) { - ("Pull Request", "closed", true) => "PR merged".to_owned(), - ("Pull Request", "closed", false) => "PR closed".to_owned(), - ("Pull Request", _, _) => "new PR".to_owned(), - ("Issue", "closed", _) => "issue closed".to_owned(), - ("Issue", _, _) => "new issue".to_owned(), + let kind = match (notif_type, notif_state) { + ("Pull", "merged") => "PR merged".to_owned(), + ("Pull", "closed") => "PR closed".to_owned(), + ("Pull", _) => "new PR".to_owned(), + ("Issue", "closed") => "issue closed".to_owned(), + ("Issue", _) => "new issue".to_owned(), _ => format!("new {notif_type}"), }; + // Fetch subject only for body/author on new (open) items — not + // worth an extra HTTP round-trip for already-closed/merged ones. + let is_open = notif_state == "open" || notif_state.is_empty(); let mut out = format!("[{kind}] {title}\nrepo: {repo}\nurl: {html_url}"); - if !body.is_empty() && !state.contains("closed") && !merged { - out.push_str(&format!( - "\n\n{author}: {}", - truncate(body, BODY_TRUNCATE) - )); + if is_open { + let subject = fetch_json(client, subject_api_url, token).await; + let author = subject + .as_ref() + .and_then(|s| s["user"]["login"].as_str()) + .unwrap_or("?"); + let body = subject + .as_ref() + .and_then(|s| s["body"].as_str()) + .unwrap_or("") + .trim(); + if !body.is_empty() { + out.push_str(&format!("\n\n{author}: {}", truncate(body, BODY_TRUNCATE))); + } } out } From 8cc63067287a4b1fc5e0901cbe7059dd9b56397b Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:13:05 +0200 Subject: [PATCH 2/3] forge_notify: generic type label helper, compose kind from label+state --- hive-ag3nt/src/forge_notify.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 048b7180..790c2045 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -105,6 +105,18 @@ async fn fetch_json( resp.json().await.ok() } +/// Map a Forgejo notification `subject.type` to a human-readable label. +/// Known values: "Pull", "Issue", "Commit", "Repository". Any unknown +/// type is passed through as-is so new Forgejo types degrade gracefully +/// rather than silently collapsing into a generic label. +fn notif_type_label(t: &str) -> &str { + match t { + "Pull" => "PR", + "Issue" => "issue", + other => other, + } +} + /// Truncate a string to `max` bytes at a char boundary, appending `…` if cut. fn truncate(s: &str, max: usize) -> String { if s.len() <= max { @@ -158,12 +170,7 @@ async fn format_notification( .unwrap_or("") .trim(); - // Forgejo API returns "Pull" / "Issue", never "Pull Request". - let kind = match notif_type { - "Pull" => "comment on PR", - "Issue" => "comment on issue", - _ => "comment", - }; + let kind = format!("comment on {}", notif_type_label(notif_type)); let mut out = format!( "[{kind}] {title}\nrepo: {repo}\nurl: {}\n\n{author}: {}\n", if comment_html_url.is_empty() { html_url } else { comment_html_url }, @@ -184,13 +191,11 @@ async fn format_notification( // - Forgejo API type is "Pull" / "Issue", never "Pull Request". let notif_state = notif["subject"]["state"].as_str().unwrap_or(""); - let kind = match (notif_type, notif_state) { - ("Pull", "merged") => "PR merged".to_owned(), - ("Pull", "closed") => "PR closed".to_owned(), - ("Pull", _) => "new PR".to_owned(), - ("Issue", "closed") => "issue closed".to_owned(), - ("Issue", _) => "new issue".to_owned(), - _ => format!("new {notif_type}"), + let label = notif_type_label(notif_type); + let kind = match notif_state { + "merged" => format!("{label} merged"), + "closed" => format!("{label} closed"), + _ => format!("new {label}"), }; // Fetch subject only for body/author on new (open) items — not From da8a711a60ca0d77b7169c4228fbcd893462feab Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:24:37 +0200 Subject: [PATCH 3/3] forge_notify: explicit open/empty state, unknown states show raw value --- hive-ag3nt/src/forge_notify.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 790c2045..2a97547a 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -195,7 +195,8 @@ async fn format_notification( let kind = match notif_state { "merged" => format!("{label} merged"), "closed" => format!("{label} closed"), - _ => format!("new {label}"), + "open" | "" => format!("new {label}"), + other => format!("{label}: {other}"), }; // Fetch subject only for body/author on new (open) items — not