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
|
- **Self-authored comments / reviews** — comment payload's
|
||||||
`user.login` matches `own_login`.
|
`user.login` matches `own_login`.
|
||||||
|
- **Self-authored creations** (an agent opening its own PR/issue) — the
|
||||||
Self-authored *new items* (an agent opening its own PR/issue) are not
|
already-fetched subject payload's poster `user.login` matches
|
||||||
filtered and do surface — the notification subject carries no author
|
`own_login`. Only *creations* are dropped; a later state change on the
|
||||||
field to match against without a per-notification fetch.
|
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
|
`own_login` is fetched once at startup via `GET /api/v1/user`. On
|
||||||
fetch failure the filter degrades open (no filtering) rather than
|
fetch failure the filter degrades open (no filtering) rather than
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ async fn format_notification(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} 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.
|
/// 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(
|
fn format_state_change_notification(
|
||||||
notif: &serde_json::Value,
|
notif: &serde_json::Value,
|
||||||
meta: &NotifMeta<'_>,
|
meta: &NotifMeta<'_>,
|
||||||
own_login: &str,
|
own_login: &str,
|
||||||
) -> String {
|
) -> Option<String> {
|
||||||
// Classification uses notif["subject"]["state"] directly — Forgejo
|
// Classification uses notif["subject"]["state"] directly — Forgejo
|
||||||
// returns "open" / "closed" / "merged" here. We do NOT rely on
|
// returns "open" / "closed" / "merged" here. We do NOT rely on
|
||||||
// fetching the PR/issue detail for `merged`:
|
// 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
|
// duplicate of the original open notification. When we can't confirm
|
||||||
// creation, fall back to a neutral "activity on" label.
|
// creation, fall back to a neutral "activity on" label.
|
||||||
let looks_new = notification_is_creation(notif, subject.as_ref());
|
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 {
|
let kind = match notif_state {
|
||||||
"merged" => format!("{label} merged{num}{repo}"),
|
"merged" => format!("{label} merged{num}{repo}"),
|
||||||
"closed" => format!("{label} closed{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}");
|
let mut out = format!("[{kind}] {title}\nurl: {html_url}{body_block}");
|
||||||
out.push_str(meta_suffix);
|
out.push_str(meta_suffix);
|
||||||
out
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decide whether a state-change notification represents the subject's
|
/// 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, None));
|
||||||
assert!(notification_is_creation(&empty, Some(&subject)));
|
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