fix(#1896): drop self-authored issue/pr creation wakes in forge_notify

This commit is contained in:
damocles 2026-06-24 22:19:32 +02:00
commit ca44fd56df
2 changed files with 74 additions and 7 deletions

View file

@ -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

View file

@ -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(&notif, &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(&notif, &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(&notif, &meta, "damocles").is_some());
}
}