import html
import io
import json
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest.mock import Mock, patch
import notify
TEST_PRIME_USER_ID = "UPRIME123"
def edge(number: int, author: str = "contributor") -> dict:
return {
"cursor": f"cursor-{number}",
"node": {
"id": f"discussion-{number}",
"number": number,
"title": f"Discussion {number}",
"author": {"login": author},
"category": {"name": "Bug reports"},
},
}
def state() -> dict:
return notify.initial_state({"pageInfo": {"endCursor": "baseline"}})
def report(number: int, author: str = "contributor") -> dict:
return {
**notify.notification_item(edge(number, author)["node"]),
"body": f"Original report {number}\n\nReproduction steps and error output.",
}
def bodies_response(*numbers: int) -> str:
return json.dumps({"data": {"nodes": [report(number) for number in numbers]}})
def trusted_run() -> dict:
return {
"event": "schedule",
"path": notify.STATE_WORKFLOW,
"head_branch": "main",
"head_repository": {"full_name": notify.SOURCE_REPOSITORY},
"conclusion": "failure",
}
class NotificationTests(unittest.TestCase):
def setUp(self):
environment = patch.dict(
"os.environ", {"PRIME_SLACK_USER_ID": TEST_PRIME_USER_ID}
)
environment.start()
self.addCleanup(environment.stop)
def test_single_notification_previews_without_webhook_or_state(self):
response = {"data": {"repository": {"discussion": edge(1)["node"]}}}
with (
patch.object(
notify, "gh", side_effect=[json.dumps(response), bodies_response(1)]
),
patch.object(notify, "deliver") as deliver,
patch.object(notify, "webhook_path") as webhook,
patch.object(notify, "restore_state") as restore,
patch("sys.argv", ["notify.py", "test", "--discussion", "1"]),
redirect_stdout(io.StringIO()) as output,
):
notify.main()
self.assertIn("/discussions/1", output.getvalue())
self.assertIn("Original report 1", output.getvalue())
self.assertIn("Preview only", output.getvalue())
deliver.assert_not_called()
webhook.assert_not_called()
restore.assert_not_called()
def test_single_notification_sends_the_production_payload_once(self):
response = {"data": {"repository": {"discussion": edge(1)["node"]}}}
with (
patch.object(
notify, "gh", side_effect=[json.dumps(response), bodies_response(1)]
),
patch.object(notify, "deliver") as deliver,
patch.object(notify, "webhook_path", return_value="/services/test"),
patch.object(notify, "restore_state") as restore,
patch.dict("os.environ", {"DRY_RUN": "false"}),
patch("sys.argv", ["notify.py", "test", "--discussion", "1", "--send"]),
redirect_stdout(io.StringIO()) as output,
):
notify.main()
deliver.assert_called_once_with(report(1), "/services/test")
restore.assert_not_called()
self.assertIn("production state is unchanged", output.getvalue())
def test_single_notification_respects_dry_run(self):
with (
patch.dict("os.environ", {"DRY_RUN": "true"}),
patch.object(notify, "gh") as gh,
patch.object(notify, "deliver") as deliver,
):
with self.assertRaisesRegex(RuntimeError, "disabled during a dry run"):
notify.test_notification(1, send=True)
gh.assert_not_called()
deliver.assert_not_called()
def test_single_notification_rejects_missing_or_invalid_discussion(self):
with (
patch.object(
notify,
"gh",
return_value=json.dumps(
{
"data": {"repository": {"discussion": None}},
}
),
) as gh,
patch.object(notify, "deliver") as deliver,
):
with self.assertRaisesRegex(RuntimeError, "positive discussion number"):
notify.test_notification(0, send=False)
gh.assert_not_called()
with self.assertRaisesRegex(RuntimeError, "Cannot read discussion #1"):
notify.test_notification(1, send=False)
deliver.assert_not_called()
def test_first_run_does_not_backfill(self):
with (
tempfile.TemporaryDirectory() as directory,
patch.object(notify, "restore_state", return_value=None),
patch.object(
notify,
"fetch_page",
return_value={
"edges": [edge(1)],
"pageInfo": {"endCursor": "cursor-1"},
},
),
patch.object(notify, "deliver") as deliver,
):
notify.plan(Path(directory), dry_run=True)
saved = json.loads((Path(directory) / "state.json").read_text())
self.assertEqual(saved["cursor"], "cursor-1")
self.assertEqual(saved["pending"], [])
self.assertEqual(
json.loads((Path(directory) / "outbox.json").read_text()), []
)
deliver.assert_not_called()
def test_plan_fetches_selected_bodies_without_saving_them_in_state(self):
current = state()
with (
tempfile.TemporaryDirectory() as directory,
patch.object(notify, "restore_state", return_value=current),
patch.object(
notify,
"fetch_page",
return_value={"edges": [edge(i) for i in (1, 2, 3)]},
),
patch.object(notify, "gh", return_value=bodies_response(2, 1)) as gh,
):
notify.plan(Path(directory), dry_run=True)
saved = json.loads((Path(directory) / "state.json").read_text())
outbox = json.loads((Path(directory) / "outbox.json").read_text())
self.assertEqual(outbox, [report(1), report(2)])
self.assertEqual([item["number"] for item in saved["pending"]], [3])
self.assertNotIn('"body"', json.dumps(saved))
self.assertEqual(
gh.call_args.kwargs["payload"]["variables"]["ids"],
["discussion-1", "discussion-2"],
)
def test_missing_body_does_not_checkpoint_reservations(self):
for response in [
{"data": {"nodes": [None]}},
{"data": {"nodes": [{"id": "discussion-1", "body": None}]}},
{"data": {"nodes": []}, "errors": [{"message": "unavailable"}]},
]:
with (
self.subTest(response=response),
tempfile.TemporaryDirectory() as directory,
patch.object(notify, "restore_state", return_value=state()),
patch.object(notify, "fetch_page", return_value={"edges": [edge(1)]}),
patch.object(notify, "gh", return_value=json.dumps(response)),
):
with self.assertRaisesRegex(RuntimeError, "state was not advanced"):
notify.plan(Path(directory), dry_run=True)
self.assertFalse((Path(directory) / "state.json").exists())
self.assertFalse((Path(directory) / "outbox.json").exists())
def test_empty_selection_does_not_fetch_bodies(self):
with patch.object(notify, "gh") as gh:
self.assertEqual(notify.fetch_report_bodies([]), [])
gh.assert_not_called()
def test_per_run_limit_preserves_pending_items(self):
current = state()
notify.enqueue(
current, {"edges": [edge(i, f"author-{i}") for i in range(1, 9)]}
)
first = notify.reserve(current, 100_000)
self.assertEqual(len(first), 5)
self.assertEqual(len(current["pending"]), 3)
second = notify.reserve(current, 100_001)
self.assertEqual(len(second), 3)
self.assertFalse(
{item["id"] for item in first} & {item["id"] for item in second}
)
def test_author_limit_cannot_be_bypassed_with_case(self):
current = state()
notify.enqueue(
current,
{
"edges": [
edge(1, "Alice"),
edge(2, "alice"),
edge(3, "ALICE"),
edge(4, "bob"),
]
},
)
self.assertEqual(
[item["number"] for item in notify.reserve(current, 100_000)], [1, 2, 4]
)
self.assertEqual([item["number"] for item in current["pending"]], [3])
self.assertEqual(notify.reserve(current, 100_001), [])
self.assertEqual(
[item["number"] for item in notify.reserve(current, 186_400)], [3]
)
def test_rolling_daily_limit_across_runs(self):
current = state()
notify.enqueue(
current, {"edges": [edge(i, f"author-{i}") for i in range(1, 26)]}
)
selected = []
for offset in range(6):
selected.extend(notify.reserve(current, 100_000 + offset))
self.assertEqual(len(selected), 20)
self.assertEqual(len(current["pending"]), 5)
self.assertEqual(notify.reserve(current, 186_399), [])
self.assertEqual(len(notify.reserve(current, 186_405)), 5)
def test_duplicate_pages_do_not_repeat_mentions(self):
current = state()
page = {"edges": [edge(1), edge(2)]}
notify.enqueue(current, page)
notify.enqueue(current, page)
self.assertEqual(len(notify.reserve(current, 100_000)), 2)
notify.enqueue(current, page)
self.assertEqual(notify.reserve(current, 100_001), [])
def test_full_queue_does_not_advance_past_unread_discussions(self):
current = state()
notify.enqueue(
current, {"edges": [edge(i) for i in range(1, notify.MAX_PENDING + 2)]}
)
self.assertEqual(len(current["pending"]), notify.MAX_PENDING)
self.assertEqual(current["cursor"], f"cursor-{notify.MAX_PENDING}")
def test_missing_author_is_handled(self):
current = state()
item = edge(1)
item["node"]["author"] = None
notify.enqueue(current, {"edges": [item]})
self.assertEqual(current["pending"][0]["author"], "deleted-user")
def test_invalid_discussion_number_is_rejected(self):
current = state()
item = edge(1)
item["node"]["number"] = "1?redirect=evil"
with self.assertRaises(RuntimeError):
notify.enqueue(current, {"edges": [item]})
self.assertEqual(current["cursor"], "baseline")
def test_message_contains_full_report_and_only_one_real_mention(self):
item = report(1, "<@UOTHER>")
item.update(
{
"title": " <@UOTHER> & " + "a" * 500,
"body": (
"## Report\n\n <@UOTHER> <@" + TEST_PRIME_USER_ID + ">"
"\n\n```typescript\nconst a = b < 5 && c > 0;\n```\n" + "x" * 4_000
),
}
)
payload = notify.slack_payload(item)
encoded = json.dumps(payload)
self.assertEqual(encoded.count("<@"), 1)
self.assertNotIn("", encoded)
self.assertEqual(html.unescape(payload["attachments"][0]["text"]), item["body"])
self.assertIn("untrusted evidence", payload["text"])
self.assertEqual(
payload["attachments"][0]["title_link"],
"https://github.com/PrimeIntellect-ai/prime-agent/discussions/1",
)
self.assertNotIn("thread_ts", payload)
self.assertFalse(payload["unfurl_links"])
def test_oversized_report_requests_manual_review_without_a_mention(self):
item = report(1)
item["body"] = "&" * (notify.MAX_REPORT_CHARS // 5)
payload = notify.slack_payload(item)
self.assertIn(f"<@{TEST_PRIME_USER_ID}>", payload["text"])
self.assertEqual(html.unescape(payload["attachments"][0]["text"]), item["body"])
item["body"] += "&"
payload = notify.slack_payload(item)
self.assertNotIn("<@", json.dumps(payload))
self.assertIn("manual review", payload["text"])
self.assertIn("no partial report", payload["attachments"][0]["text"])
self.assertTrue(
payload["attachments"][0]["title_link"].endswith("/discussions/1")
)
def test_bot_configuration_cannot_inject_mentions(self):
for value in ["", "prime", "", "UPRIME123> <@UOTHER", "U123\n"]:
with (
self.subTest(value=value),
patch.dict("os.environ", {"PRIME_SLACK_USER_ID": value}),
):
with self.assertRaisesRegex(RuntimeError, "PRIME_SLACK_USER_ID"):
notify.slack_payload(report(1))
def test_missing_bot_configuration_does_not_reserve_discussions(self):
with (
patch.dict("os.environ", {"PRIME_SLACK_USER_ID": ""}),
patch.object(notify, "webhook_path", return_value="/services/test"),
patch.object(notify, "restore_state") as restore,
):
with self.assertRaisesRegex(RuntimeError, "PRIME_SLACK_USER_ID"):
notify.plan(Path("unused"), dry_run=False)
restore.assert_not_called()
def test_webhook_is_restricted_to_slack_incoming_webhooks(self):
for url in [
"",
"https://example.com/services/test",
"http://hooks.slack.com/services/test",
"https://hooks.slack.com/triggers/test",
"https://hooks.slack.com/services/test?token=secret",
]:
with (
self.subTest(url=url),
patch.dict("os.environ", {"SLACK_WEBHOOK_URL": url}),
):
with self.assertRaisesRegex(
RuntimeError, "Configure SLACK_DISCUSSION_WEBHOOK_URL"
):
notify.webhook_path()
with patch.dict(
"os.environ", {"SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/test"}
):
self.assertEqual(notify.webhook_path(), "/services/test")
def test_ambiguous_slack_failure_is_not_retried(self):
current = state()
notify.enqueue(current, {"edges": [edge(1)]})
item = notify.reserve(current, 100_000)[0]
item["body"] = report(1)["body"]
with patch.object(notify.http.client, "HTTPSConnection") as connection:
connection.return_value.getresponse.side_effect = TimeoutError(
"sensitive endpoint"
)
with self.assertRaisesRegex(
RuntimeError, "delivery for discussion #1 is uncertain"
):
notify.deliver(item, "/services/secret")
connection.return_value.request.assert_called_once()
connection.return_value.close.assert_called_once()
self.assertEqual(notify.reserve(current, 100_001), [])
def test_slack_requires_success_status_and_body(self):
item = report(1)
for status, body, succeeds in [
(200, b"ok", True),
(200, b"invalid_payload", False),
(429, b"rate_limited", False),
]:
with (
self.subTest(status=status, body=body),
patch.object(notify.http.client, "HTTPSConnection") as connection,
):
connection.return_value.getresponse.return_value = Mock(
status=status, read=Mock(return_value=body)
)
if succeeds:
notify.deliver(item, "/services/test")
else:
with self.assertRaises(RuntimeError):
notify.deliver(item, "/services/test")
connection.return_value.request.assert_called_once()
def test_restores_latest_trusted_reservation_by_creation_time(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)
saved = state()
(path / "state.json").write_text(json.dumps(saved))
artifacts = [
{
"id": artifact_id,
"expired": False,
"created_at": created_at,
"workflow_run": {"id": run_id, "head_branch": branch},
}
for artifact_id, run_id, branch, created_at in [
(300, 10, "main", "2026-09-12T01:00:00Z"),
(100, 30, "feature/test", "2026-09-12T03:00:00Z"),
(200, 20, "main", "2026-09-12T02:00:00Z"),
]
]
with patch.object(
notify,
"gh",
side_effect=[
json.dumps({"artifacts": artifacts}),
json.dumps(trusted_run()),
"",
],
) as gh:
self.assertEqual(notify.restore_state(path), saved)
self.assertIn("20", gh.call_args.args)
def test_untrusted_builds_cannot_replace_notification_state(self):
for change in [
{"event": "pull_request"},
{"event": "pull_request_target"},
{"path": ".github/workflows/other.yml"},
{"head_repository": {"full_name": "contributor/prime-agent"}},
{"head_branch": "feature/test"},
]:
with (
self.subTest(change=change),
tempfile.TemporaryDirectory() as directory,
):
path = Path(directory)
saved = state()
(path / "state.json").write_text(json.dumps(saved))
artifacts = [
{
"id": i,
"expired": False,
"created_at": f"2026-09-12T00:0{i}:00Z",
"workflow_run": {"id": i * 10, "head_branch": "main"},
}
for i in [1, 2]
]
with patch.object(
notify,
"gh",
side_effect=[
json.dumps({"artifacts": artifacts}),
json.dumps({**trusted_run(), **change}),
json.dumps({**trusted_run(), "event": "workflow_dispatch"}),
"",
],
) as gh:
self.assertEqual(notify.restore_state(path), saved)
downloads = [
call for call in gh.call_args_list if call.args[0] == "run"
]
self.assertEqual(len(downloads), 1)
self.assertEqual(downloads[0].args[2], "10")
def test_expired_state_is_not_silently_reset(self):
artifact = {
"id": 1,
"expired": True,
"created_at": "2026-09-12T00:01:00Z",
"workflow_run": {"id": 10, "head_branch": "main"},
}
with patch.object(
notify,
"gh",
side_effect=[
json.dumps({"artifacts": [artifact]}),
json.dumps(trusted_run()),
],
):
with self.assertRaisesRegex(RuntimeError, "state expired"):
notify.restore_state(Path("unused"))
if __name__ == "__main__":
unittest.main()