fix(#1896): drop self-authored issue/pr creation wakes in forge_notify
This commit is contained in:
parent
c68706e899
commit
ca44fd56df
2 changed files with 74 additions and 7 deletions
|
|
@ -141,10 +141,10 @@ silently (mark-read without delivery):
|
|||
|
||||
- **Self-authored comments / reviews** — comment payload's
|
||||
`user.login` matches `own_login`.
|
||||
|
||||
Self-authored *new items* (an agent opening its own PR/issue) are not
|
||||
filtered and do surface — the notification subject carries no author
|
||||
field to match against without a per-notification fetch.
|
||||
- **Self-authored creations** (an agent opening its own PR/issue) — the
|
||||
already-fetched subject payload's poster `user.login` matches
|
||||
`own_login`. Only *creations* are dropped; a later state change on the
|
||||
agent's own subject is driven by someone else and still surfaces.
|
||||
|
||||
`own_login` is fetched once at startup via `GET /api/v1/user`. On
|
||||
fetch failure the filter degrades open (no filtering) rather than
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ async fn format_notification(
|
|||
)
|
||||
.await
|
||||
} else {
|
||||
Some(format_state_change_notification(notif, &meta, own_login))
|
||||
format_state_change_notification(notif, &meta, own_login)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -553,11 +553,16 @@ async fn format_comment_notification(
|
|||
}
|
||||
|
||||
/// Format a notification triggered by creation or state change of the subject.
|
||||
///
|
||||
/// Returns `None` for an agent's own *creation* (it opened the issue/PR) —
|
||||
/// the same "don't loop claude on its own writes" rule the comment/review
|
||||
/// path applies. Only creations are dropped: a later state change on the
|
||||
/// agent's own subject is driven by someone else and stays a wake.
|
||||
fn format_state_change_notification(
|
||||
notif: &serde_json::Value,
|
||||
meta: &NotifMeta<'_>,
|
||||
own_login: &str,
|
||||
) -> String {
|
||||
) -> Option<String> {
|
||||
// Classification uses notif["subject"]["state"] directly — Forgejo
|
||||
// returns "open" / "closed" / "merged" here. We do NOT rely on
|
||||
// fetching the PR/issue detail for `merged`:
|
||||
|
|
@ -590,6 +595,22 @@ fn format_state_change_notification(
|
|||
// duplicate of the original open notification. When we can't confirm
|
||||
// creation, fall back to a neutral "activity on" label.
|
||||
let looks_new = notification_is_creation(notif, subject.as_ref());
|
||||
|
||||
// Self-authored creation filter: skip an agent being woken by its own
|
||||
// freshly-opened issue/PR. The subject payload is already fetched (for
|
||||
// assignees / reviewers / body), so its poster `user.login` costs no
|
||||
// extra request. Mirrors the self-authored comment/review drop above.
|
||||
if looks_new && !own_login.is_empty() {
|
||||
let author = subject
|
||||
.as_ref()
|
||||
.and_then(|s| s["user"]["login"].as_str())
|
||||
.unwrap_or("");
|
||||
if author == own_login {
|
||||
debug!(%own_login, "forge_notify: skipping self-authored creation");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let kind = match notif_state {
|
||||
"merged" => format!("{label} merged{num}{repo}"),
|
||||
"closed" => format!("{label} closed{num}{repo}"),
|
||||
|
|
@ -635,7 +656,7 @@ fn format_state_change_notification(
|
|||
|
||||
let mut out = format!("[{kind}] {title}\nurl: {html_url}{body_block}");
|
||||
out.push_str(meta_suffix);
|
||||
out
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Decide whether a state-change notification represents the subject's
|
||||
|
|
@ -1162,4 +1183,50 @@ mod tests {
|
|||
assert!(notification_is_creation(&empty, None));
|
||||
assert!(notification_is_creation(&empty, Some(&subject)));
|
||||
}
|
||||
|
||||
/// Build a `NotifMeta` for the state-change formatter tests. The `&str`
|
||||
/// fields borrow `'static` literals so the value is self-contained.
|
||||
fn state_change_meta(subject: serde_json::Value) -> NotifMeta<'static> {
|
||||
NotifMeta {
|
||||
title: "subject title",
|
||||
notif_type: "Issue",
|
||||
html_url: "http://forge/issues/1",
|
||||
num: " #1".to_owned(),
|
||||
repo: " [agents/x]".to_owned(),
|
||||
meta_suffix: "\nassignee: unassigned".to_owned(),
|
||||
subject: Some(subject),
|
||||
is_pr: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_change_drops_self_authored_creation() {
|
||||
// No timestamps ⇒ treated as a creation; poster login == own_login.
|
||||
let meta = state_change_meta(serde_json::json!({ "user": { "login": "damocles" } }));
|
||||
let notif = serde_json::json!({ "subject": { "state": "open" } });
|
||||
assert!(format_state_change_notification(¬if, &meta, "damocles").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_change_keeps_other_authored_creation() {
|
||||
let meta = state_change_meta(serde_json::json!({ "user": { "login": "someone-else" } }));
|
||||
let notif = serde_json::json!({ "subject": { "state": "open" } });
|
||||
assert!(format_state_change_notification(¬if, &meta, "damocles").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_change_keeps_self_authored_later_activity() {
|
||||
// The agent authored the subject, but this notification fired well
|
||||
// after creation (someone else acted on it) ⇒ not a creation ⇒ still
|
||||
// surfaces.
|
||||
let meta = state_change_meta(serde_json::json!({
|
||||
"user": { "login": "damocles" },
|
||||
"created_at": "2020-01-01T00:00:00Z",
|
||||
}));
|
||||
let notif = serde_json::json!({
|
||||
"subject": { "state": "closed" },
|
||||
"updated_at": "2026-06-22T16:00:00Z",
|
||||
});
|
||||
assert!(format_state_change_notification(¬if, &meta, "damocles").is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue