{ "_what_this_is": [ "Fixtures for the pull-request-mention trigger in infra/agent/pm-dispatch.py, driven by", "`PM_SELFTEST=1 pm-dispatch` (and therefore by the Containerfile's verify stage and by", "infra/agent/build-image.sh). Each case feeds normalize_pr_entries() + plan_pr_replies()", "and asserts the EXACT set of threads that would be answered, with which comment keys.", "", "HOW HONEST THESE ARE, stated plainly because it matters for what a green selftest", "proves. These objects are written to the field names and types in Gitea's published", "swagger for the 1.24 line (Comment, PullReview, PullReviewComment, NotificationThread).", "They were NOT recorded from a live server: the branch that wrote them held no Gitea", "credential. So a passing case proves that this program's PARSING and TRIGGER LOGIC are", "right for that shape -- the loop guard, the idempotency skip, the continuation policy --", "and proves nothing about whether Gitea emits that shape. Only unused-and-wrong field", "names can hide here, and the remedy is cheap: capture one real response with the curl", "commands in platform/pm-agent/README.md and replace the objects below with it. Fields", "this program never reads are kept anyway, so the diff against a real capture is small.", "", "Only the fields the dispatcher reads are load-bearing:", " Comment ............. id, user.login, created_at, body", " PullReview .......... id, user.login, submitted_at, body, comments_count", " PullReviewComment ... id, user.login, created_at, body, path, position", "", "`expect.threads` is in the order plan_pr_replies returns: NEWEST-ANCHORED FIRST.", "", "THE CONTINUATION POLICY THESE PIN. A thread becomes a conversation the PM is part of", "at the first mention in it (or at a marker in one of the PM's own comments), and every", "later non-bot comment in that thread then triggers without a new mention -- the", "operator's wording on issue #116, answer (4'). Cases (i) and (o) are the two edges:", "comments AFTER the mention are in, comments BEFORE it are not. Case (l) pins that this", "is per thread, not per pull request." ], "bot_login": "pm-agent", "cases": [ { "name": "(a) a mention in a PR conversation comment is detected", "pull": 120, "comments": [ { "id": 9001, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T09:00:00Z", "updated_at": "2026-11-07T09:00:00Z", "body": "@pm-agent can you look at the retry loop in cycle() here? I think it double-counts." } ], "reviews": [], "review_comments": {}, "expect": { "threads": [ {"thread": "conversation", "reason": "mention", "keys": ["comment-9001"]} ] } }, { "name": "(b) a mention in an inline review comment is detected, as its own thread", "pull": 121, "comments": [], "reviews": [ { "id": 400, "user": {"login": "jakman85", "id": 1}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T09:10:00Z", "comments_count": 1 } ], "review_comments": { "400": [ { "id": 7701, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T09:10:00Z", "body": "@pm-agent this cap looks off by one.", "path": "infra/agent/pm-dispatch.py", "position": 42, "original_position": 42, "pull_request_review_id": 400, "diff_hunk": "@@ -1,3 +1,4 @@" } ] }, "expect": { "threads": [ { "thread": "inline:infra/agent/pm-dispatch.py@42", "reason": "mention", "keys": ["review-comment-7701"] } ] } }, { "name": "(c) a comment authored by the bot itself is ignored, mention and all", "pull": 122, "comments": [ { "id": 9010, "user": {"login": "pm-agent", "id": 7}, "created_at": "2026-11-07T09:20:00Z", "body": "Worth a look: @pm-agent should re-read this once the gate is green." }, { "id": 9011, "user": {"login": "pm-agent", "id": 7}, "created_at": "2026-11-07T09:21:00Z", "body": "A plain status note from the agent, no mention at all." } ], "reviews": [], "review_comments": {}, "expect": {"threads": []} }, { "name": "(d) an already-processed comment id is skipped", "pull": 123, "comments": [ { "id": 9020, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T09:30:00Z", "body": "@pm-agent what does the approval stage need here?" }, { "id": 9021, "user": {"login": "pm-agent", "id": 7}, "created_at": "2026-11-07T09:31:00Z", "body": "It needs a review from someone who is not the author.\n\n_Posted by the PM agent._\n\n" } ], "reviews": [], "review_comments": {}, "expect": {"threads": []} }, { "name": "(e) a follow-up with NO mention continues a thread the bot has replied in", "pull": 124, "comments": [ { "id": 9030, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T09:40:00Z", "body": "@pm-agent is this safe to merge on a green gate?" }, { "id": 9031, "user": {"login": "pm-agent", "id": 7}, "created_at": "2026-11-07T09:41:00Z", "body": "Yes, nothing here is a sensitive path.\n\n" }, { "id": 9032, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T09:45:00Z", "body": "And the conformance gap? No mention in this one on purpose." } ], "reviews": [], "review_comments": {}, "expect": { "threads": [ {"thread": "conversation", "reason": "continuation", "keys": ["comment-9032"]} ] } }, { "name": "(f) an unrelated comment on an unrelated PR triggers nothing", "pull": 125, "comments": [ { "id": 9040, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T09:50:00Z", "body": "Rebased onto main; the cilium bump is in." }, { "id": 9041, "user": {"login": "renovate", "id": 9}, "created_at": "2026-11-07T09:51:00Z", "body": "Edited/Blocked: this PR is edited, Renovate will not update it." } ], "reviews": [ { "id": 401, "user": {"login": "jakman85", "id": 1}, "body": "Looks fine to me.", "state": "COMMENT", "submitted_at": "2026-11-07T09:52:00Z", "comments_count": 0 } ], "review_comments": {}, "expect": {"threads": []} }, { "name": "(g) a near-miss handle is not a mention: @pm-agent-worker and an email-like form", "pull": 126, "comments": [ { "id": 9050, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:00:00Z", "body": "The Job runs as @pm-agent-worker, and the log line was mailed to ops@pm-agent.example. Also pm-agent without the at-sign." } ], "reviews": [], "review_comments": {}, "expect": {"threads": []} }, { "name": "(h) a marker pasted by someone who is NOT the bot does not suppress a mention", "pull": 127, "comments": [ { "id": 9060, "user": {"login": "drive-by", "id": 4}, "created_at": "2026-11-07T10:10:00Z", "body": "@pm-agent read this.\n\n" } ], "reviews": [], "review_comments": {}, "expect": { "threads": [ {"thread": "conversation", "reason": "mention", "keys": ["comment-9060"]} ] } }, { "name": "(i) a burst of three unanswered comments is ONE reply covering all three keys", "pull": 128, "comments": [ { "id": 9070, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:20:00Z", "body": "@pm-agent one question:" }, { "id": 9071, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:20:30Z", "body": "actually two." }, { "id": 9072, "user": {"login": "someone-else", "id": 5}, "created_at": "2026-11-07T10:21:00Z", "body": "three, sorry." } ], "reviews": [], "review_comments": {}, "expect": { "threads": [ { "thread": "conversation", "reason": "mention", "keys": ["comment-9070", "comment-9071", "comment-9072"] } ] } }, { "name": "(j) a mention in a review BODY is a conversation trigger, not an inline one", "pull": 129, "comments": [], "reviews": [ { "id": 402, "user": {"login": "jakman85", "id": 1}, "body": "@pm-agent summarise what this changes, please.", "state": "REQUEST_CHANGES", "submitted_at": "2026-11-07T10:30:00Z", "comments_count": 0 } ], "review_comments": {}, "expect": { "threads": [ {"thread": "conversation", "reason": "mention", "keys": ["review-402"]} ] } }, { "name": "(k) two inline threads on one file are separate threads; the newest anchor comes first", "pull": 130, "comments": [], "reviews": [ { "id": 403, "user": {"login": "jakman85", "id": 1}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T10:40:00Z", "comments_count": 2 } ], "review_comments": { "403": [ { "id": 7710, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:40:00Z", "body": "@pm-agent why this order?", "path": "ci/gate.sh", "position": 10, "pull_request_review_id": 403 }, { "id": 7711, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:41:00Z", "body": "@pm-agent and this skip?", "path": "ci/gate.sh", "position": 55, "pull_request_review_id": 403 } ] }, "expect": { "threads": [ {"thread": "inline:ci/gate.sh@55", "reason": "mention", "keys": ["review-comment-7711"]}, {"thread": "inline:ci/gate.sh@10", "reason": "mention", "keys": ["review-comment-7710"]} ] } }, { "name": "(l) continuation is PER THREAD: a reply in the conversation does not open the inline thread", "pull": 131, "comments": [ { "id": 9080, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:50:00Z", "body": "@pm-agent have a look." }, { "id": 9081, "user": {"login": "pm-agent", "id": 7}, "created_at": "2026-11-07T10:51:00Z", "body": "Looked.\n\n" } ], "reviews": [ { "id": 404, "user": {"login": "jakman85", "id": 1}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T10:55:00Z", "comments_count": 1 } ], "review_comments": { "404": [ { "id": 7720, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T10:55:00Z", "body": "no mention here, and the bot has never replied in THIS thread", "path": "ci/lint.sh", "position": 3, "pull_request_review_id": 404 } ] }, "expect": {"threads": []} }, { "name": "(m) an inline thread the bot has replied in continues without a mention", "pull": 132, "comments": [], "reviews": [ { "id": 405, "user": {"login": "jakman85", "id": 1}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T11:00:00Z", "comments_count": 1 }, { "id": 406, "user": {"login": "pm-agent", "id": 7}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T11:01:00Z", "comments_count": 1 }, { "id": 407, "user": {"login": "jakman85", "id": 1}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T11:05:00Z", "comments_count": 1 } ], "review_comments": { "405": [ { "id": 7730, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T11:00:00Z", "body": "@pm-agent is this cap right?", "path": "platform/pm-agent/configmap.yaml", "position": 88, "pull_request_review_id": 405 } ], "406": [ { "id": 7731, "user": {"login": "pm-agent", "id": 7}, "created_at": "2026-11-07T11:01:00Z", "body": "It matches the admission policy.\n\n", "path": "platform/pm-agent/configmap.yaml", "position": 88, "pull_request_review_id": 406 } ], "407": [ { "id": 7732, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T11:05:00Z", "body": "and the worker cap?", "path": "platform/pm-agent/configmap.yaml", "position": 88, "pull_request_review_id": 407 } ] }, "expect": { "threads": [ { "thread": "inline:platform/pm-agent/configmap.yaml@88", "reason": "continuation", "keys": ["review-comment-7732"] } ] } }, { "name": "(o) a comment made BEFORE the mention is not pulled into the conversation", "pull": 134, "comments": [ { "id": 9090, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T11:20:00Z", "body": "Opening this for review." }, { "id": 9091, "user": {"login": "someone-else", "id": 5}, "created_at": "2026-11-07T11:21:00Z", "body": "LGTM." }, { "id": 9092, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T11:22:00Z", "body": "@pm-agent what do you think?" } ], "reviews": [], "review_comments": {}, "expect": { "threads": [ {"thread": "conversation", "reason": "mention", "keys": ["comment-9092"]} ] } }, { "name": "(n) a position-less inline comment still lands in a stable thread (review-id fallback)", "pull": 133, "comments": [], "reviews": [ { "id": 408, "user": {"login": "jakman85", "id": 1}, "body": "", "state": "COMMENT", "submitted_at": "2026-11-07T11:10:00Z", "comments_count": 1 } ], "review_comments": { "408": [ { "id": 7740, "user": {"login": "jakman85", "id": 1}, "created_at": "2026-11-07T11:10:00Z", "body": "@pm-agent the whole file, not a line.", "path": "runbooks/pm-agent.md", "pull_request_review_id": 408 } ] }, "expect": { "threads": [ { "thread": "inline:runbooks/pm-agent.md@review-408", "reason": "mention", "keys": ["review-comment-7740"] } ] } } ], "notification_subjects": [ { "_note": "NotificationThread.subject shapes discover_pr_candidates must resolve to an index.", "url": "http://gitea.gitea.svc.cluster.local:3000/api/v1/repos/Home-Network/home-network-k3s-impl/issues/120", "expect_index": 120 }, { "url": "http://gitea.gitea.svc.cluster.local:3000/api/v1/repos/Home-Network/home-network-k3s-impl/pulls/121", "expect_index": 121 }, { "_note": "A comment-anchored URL must still yield the PR index, never the comment id.", "url": "http://gitea.gitea.svc.cluster.local:3000/api/v1/repos/Home-Network/home-network-k3s-impl/issues/122/comments/9001", "expect_index": 122 }, { "_note": "Nothing resolvable: discovery must skip it rather than guess.", "url": "http://gitea.gitea.svc.cluster.local:3000/api/v1/repos/Home-Network/home-network-k3s-impl", "expect_index": null } ] }